Symfony EventSauce (WIP)
This bundle provides the basic and extended container configuration of symfony for the EventSauce library. Before using it, I strongly recommend that you read the official documentation.
Supports
- Doctrine event message repository
- All events in table per aggregate type
- Message Outbox
- Symfony messenger
- Symfony serializer
- Snapshot doctrine repository
- Snapshot versioning
- Snapshot store every n event
- Automatic generate migration for aggregate
- Message upcasting
Requirements
- PHP ^8.1
- Symfony ^6.0
Installation
composer require andreo/event-sauce-bundle
Verify that the bundle has been added the config/bundles.php
file
return [
Andreo\EventSauceBundle\AndreoEventSauceBundle::class => ['all' => true],
];
Timezone
You probably want to set your time zone.
andreo_event_sauce:
time:
recording_timezone: Europe/Warsaw # default is UTC
Message dispatching
Defaults EventSauce to dispatch events use SynchronousMessageDispatcher. An example configuration is as follows
andreo_event_sauce:
message:
dispatcher:
chain:
- fooBus
- barBus
Defining the example message consumer is as follows
use EventSauce\EventSourcing\MessageConsumer;
use Andreo\EventSauceBundle\Attribute\AsMessageConsumer;
#[AsMessageConsumer(dispatcher: fooBus)]
final class SomeProjection implements MessageConsumer {
public function handle(Message $message): void {
// do something
}
}
Message dispatching with symfony messenger
You need install the package (recommend reading doc).
composer require andreo/eventsauce-messenger
An example configuration is as follows
andreo_event_sauce:
message:
dispatcher:
messenger: true
chain:
fooBus: barBus # message bus alias from messenger config
bazBus: quxBus
Message dispatching mode
The mode option is a way of dispatch messages. Available values:
event
(default)
- Event is only dispatch to the handler that supports the event type
- Doesn't dispatch headers
event_and_headers
- Event is dispatch to the handler that supports the event type
- Receive of message headers in the second handler argument
message
- Message is dispatch to the any handler that supports the Message type. You have to manually check event type
- Message object includes the event and headers
Change the default event mode
andreo_event_sauce:
message:
dispatcher:
messenger:
mode: event_and_headers # default is event
chain:
fooBus: barBus
Event Dispatcher
Sometimes you may want to use dispatch messages in a context other than aggregate. You can do this with the Event Dispatcher
To enable it use this configuration
andreo_event_sauce:
message:
dispatcher:
event_dispatcher: true # default is false
chain:
- fooBus
Then you can inject the event dispatcher based on the alias and dedicated interface
use EventSauce\EventSourcing\EventDispatcher;
use Symfony\Component\DependencyInjection\Attribute\Target;
final class SomeContext {
public function __construct(
#[Target('fooBus')] private EventDispatcher $fooBus
){}
}
Aggregates
An example configuration for two aggregates is as follows
andreo_event_sauce:
aggregates:
foo:
class: App\Domain\Foo
bar:
class: App\Domain\Bar
Then you can inject the repository based on the alias and dedicated interface. By default, alias is created automatically by convention "${name}Repository"
use EventSauce\EventSourcing\AggregateRootRepository;
use Symfony\Component\DependencyInjection\Attribute\Target;
final class SomeHandler {
public function __construct(
#[Target('fooRepository')] private AggregateRootRepository $fooRepository
){}
}
Message dispatching
By default, messages are dispatch by all dispatchers, but you can specify them per aggregate.
andreo_event_sauce:
message:
dispatcher:
chain:
- fooBus
- barBus
aggregates:
foo:
class: App\Domain\Foo
dispatchers:
- barBus # dispatch only by barBas
Outbox
You need install the package (recommend reading doc).
composer require andreo/eventsauce-outbox
andreo_event_sauce:
outbox: true # enable outbox and register its services
aggregates:
foo:
class: App\Domain\Foo
outbox: true # register doctrine transactional repository and outbox relay per aggregate
Outbox Back-off Strategy
About the Back-off Strategy
By default, this bundle uses ExponentialBackOffStrategy. You can change it. An example configuration is as follows
andreo_event_sauce:
outbox:
back_off:
fibonacci: # default is exponential. More data in config reference
initial_delay_ms: 100000
max_tries: 10
Outbox Commit Strategy
By default, this bundle uses MarkMessagesConsumedOnCommit strategy You can change it. An example configuration is as follows
andreo_event_sauce:
outbox:
relay_commit:
delete: true # default is mark_consumed
Outbox repository
By default, outbox messages are stored in a database. If you want to store them in a memory, add the following configuration
andreo_event_sauce:
outbox:
repository:
memory: true # default is doctrine
Outbox messages dispatching command
php bin/console andreo:event-sauce:outbox-process-messages
Snapshotting
andreo_event_sauce:
snapshot: true # enable snapshot and register its services
aggregates:
bar:
class: App\Domain\Bar
snapshot: true # register snapshot repository per aggregate
Then you can inject the repository based on the alias and dedicated interface
use Symfony\Component\DependencyInjection\Attribute\Target;
use EventSauce\EventSourcing\Snapshotting\AggregateRootRepositoryWithSnapshotting;
final class SomeHandler {
public function __construct(
#[Target('barRepository')] private AggregateRootRepositoryWithSnapshotting $barRepository
){}
}
Snapshotting extended components
You need install the package (recommend reading doc).
composer require andreo/eventsauce-snapshotting
Snapshot Doctrine repository
By default, snapshots are stored in a memory. If you want to store them in a database with doctrine, add the following configuration
andreo_event_sauce:
snapshot:
repository:
doctrine: true # default is memory
aggregates:
foo:
class: App\Domain\Foo
snapshot: true
Snapshot versioning
andreo_event_sauce:
snapshot:
versioned: true # default is false
aggregates:
foo:
class: App\Domain\Foo
snapshot: true
Snapshot store strategy
andreo_event_sauce:
snapshot:
store_strategy:
every_n_event: # Store snapshot every n event
number: 200
custom: # Or you can set your own strategy
aggregates:
foo:
class: App\Domain\Foo
snapshot: true
Upcasting
andreo_event_sauce:
upcast: true # enable upcast and register its services
aggregates:
foo:
class: App\Domain\Foo
upcast: true # register upcaster chain per aggregate
Defining the upcaster is as follows
use Andreo\EventSauceBundle\Attribute\AsUpcaster;
use EventSauce\EventSourcing\Upcasting\Upcaster;
#[AsUpcaster(aggregate: 'foo', version: 2)]
final class SomeEventV2Upcaster implements Upcaster {
public function upcast(array $message): array
{
// do something
}
}
Upcaster context
By default, this library uses the payload context according to the EventSauce implementation. If you want to upcasting on the message object context, you need install the package (recommend reading doc).
composer require andreo/eventsauce-upcasting
and use the following configuration.
andreo_event_sauce:
upcast:
context: message # default is payload
aggregates:
foo:
class: App\Domain\Foo
upcast: true
Defining the upcaster is as follows
use Andreo\EventSauce\Upcasting\Event;
use Andreo\EventSauce\Upcasting\MessageUpcaster;
use EventSauce\EventSourcing\Message;
#[AsUpcaster(aggregate: 'foo', version: 2)]
final class SomeEventV2Upcaster implements MessageUpcaster {
#[Event(event: SomeEvent::class)] // guess event
public function upcast(Message $message): Message
{
// do something
}
}
Message decorating
About Message Decorator
Defining the message decorator is as follows
use Andreo\EventSauceBundle\Attribute\AsMessageDecorator;
use EventSauce\EventSourcing\Message;
use EventSauce\EventSourcing\MessageDecorator;
#[AsMessageDecorator]
final class SomeDecorator implements MessageDecorator
{
public function decorate(Message $message): Message
{
// do something
}
}
By default, message decoration is enabled. You can it disabled.
andreo_event_sauce:
message:
decorator: false
Message decorating context
In this bundle, messages can be decorated at the aggregate level, or at a completely different level of the event dispatcher. You can specify the context in which the decorator is to be used
use Andreo\EventSauceBundle\Attribute\AsMessageDecorator;
use EventSauce\EventSourcing\Message;
use EventSauce\EventSourcing\MessageDecorator;
use Andreo\EventSauceBundle\Attribute\MessageContext;
#[AsMessageDecorator(context: MessageContext::EVENT_DISPATCHER)]
final class SomeDecorator implements MessageDecorator
{
public function decorate(Message $message): Message
{
// do something
}
}
Database Structure
This bundle uses the all events in table per aggregate approach. Event messages, outbox messages, and snapshots are stored in a separate table per aggregate type
To simplify the creation of migrations, I have created a package that allows you to automatically generate migrations based on the name of the aggregate. More information in the documentation
composer require andreo/eventsauce-generate-migration
For example, to generate migrations for the following configuration
andreo_event_sauce:
outbox: true
aggregates:
foo:
class: App\Domain\Foo
outbox: true
Execute the following command
php bin/console andreo:event-sauce:doctrine:migration:generate foo --schema=event --schema=outbox
and default doctrine migration command
php bin/console d:m:m
Serialization
Symfony payload serializer
The default serializer is ConstructingPayloadSerializer
If you don't want to build payload yourself, you can use the symfony serializer
You need install the package.
composer require andreo/eventsauce-symfony-serializer
and add the following configuration
andreo_event_sauce:
payload_serializer: Andreo\EventSauce\Serialization\SymfonyPayloadSerializer # or your custom serializer
Message serializer for MySQL8
andreo_event_sauce:
message:
serializer: EventSauce\EventSourcing\Serialization\MySQL8DateFormatting # or your custom serializer