Official documentation of eventsauce
- Doctrine3 event store
- Symfony messenger message dispatcher
- Anti-Corruption Layer
- Event dispatcher
- Message Outbox
- Snapshot doctrine repository, versioning, conditional persist
- All events in table per aggregate type
- Generating migrations per aggregate
- PHP >=8.2
- Symfony ^6.2
composer require andreo/eventsauce-bundle// config/bundles.php
return [
Andreo\EventSauceBundle\AndreoEventSauceBundle::class => ['all' => true],
];Below configs presents default values and some example values.
Note that most of default config values do not need to configure.
andreo_event_sauce:
clock:
timezone: UTCUseful aliases
EventSauce\Clock\Clock: EventSauce\Clock\SystemClockandreo_event_sauce:
#...
message_storage:
repository:
doctrine_3:
enabled: true
json_encode_flags: []
connection: doctrine.dbal.default_connection
table_name: event_storeRequire
- doctrine/dbal
andreo_event_sauce:
#...
message_dispatcher: # chain of message dispatchers
foo_dispatcher:
type:
sync: true
bar_dispatcher:
type:
sync: trueuse EventSauce\EventSourcing\MessageConsumer;
use Andreo\EventSauceBundle\Attribute\AsSyncMessageConsumer;
use EventSauce\EventSourcing\EventConsumption\EventConsumer;
use Andreo\EventSauce\Messenger\EventConsumer\InjectedHandleMethodInflector;
use EventSauce\EventSourcing\Message;
#[AsSyncMessageConsumer(dispatcher: 'foo_dispatcher')]
final class FooBarEventConsumer extends EventConsumer
{
// copy-paste trait for inject HandleMethodInflector of EventSauce
use InjectedHandleMethodInflector;
public function __construct(
private HandleMethodInflector $handleMethodInflector
){}
public function onFooCreated(FooCreated $fooCreated, Message $message): void {
}
public function onBarCreated(BarCreated $barCreated, Message $message): void {
}
}Example of manually registration sync consumer
(without attribute and autoconfiguration)
services:
#...
App\Consumer\FooBarEventConsumer:
tags:
-
name: andreo.eventsauce.sync_message_consumerDispatching with Symfony messenger
Install andreo/eventsauce-messenger
composer require andreo/eventsauce-messengerandreo_event_sauce:
#...
message_dispatcher: # chain of message dispatchers
foo_dispatcher:
type:
messenger:
bus: event_bus # bus alias from messenger configIt registers alias of handle event sauce message middleware:
$busAlias.handle_eventsauce_message: Andreo\EventSauce\Messenger\Middleware\HandleEventSauceMessageMiddlewareUpdate messenger config. According to above config
framework:
messenger:
#...
buses:
event_bus:
default_middleware: false # disable default middleware order
middleware:
- 'add_bus_name_stamp_middleware': ['event_bus']
- 'dispatch_after_current_bus'
- 'failed_message_processing_middleware'
- 'send_message'
- 'event_bus.handle_eventsauce_message' # our middleware should be placed after send_message and before default handle massage middleware (if you use)
- 'handle_message'use Andreo\EventSauce\Messenger\EventConsumer\InjectedHandleMethodInflector;
use EventSauce\EventSourcing\EventConsumption\EventConsumer;
use EventSauce\EventSourcing\EventConsumption\HandleMethodInflector;
use Andreo\EventSauce\Messenger\Attribute\AsEventSauceMessageHandler;
use EventSauce\EventSourcing\Message;
final class FooBarEventConsumer extends EventConsumer
{
use InjectedHandleMethodInflector;
public function __construct(
private HandleMethodInflector $handleMethodInflector
)
{}
#[AsEventSauceMessageHandler(bus: 'fooBus')]
public function onFooCreated(FooCreated $fooCreated, Message $message): void
{
}
#[AsEventSauceMessageHandler(bus: 'barBus')]
public function onBarCreated(BarCreated $barCreated, Message $message): void
{
}
}Useful aliases
EventSauce\EventSourcing\EventConsumption\HandleMethodInflector: EventSauce\EventSourcing\EventConsumption\InflectHandlerMethodsFromTypeMessage dispatcher tag (for manually registration of dispatchers, if you will want)
andreo.eventsauce.message_dispatcherandreo_event_sauce:
#...
acl: trueEnable for Message dispatcher (by config)
andreo_event_sauce:
#...
message_dispatcher:
fooDispatcher:
type:
messenger:
bus: fooBus
acl:
enabled: true
message_filter_strategy:
before_translate: match_all # or match_any
after_translate: match_all # or match_any
Enable for Message consumer
use Andreo\EventSauceBundle\Attribute\EnableAcl;
use Andreo\EventSauceBundle\Enum\MessageFilterStrategy;
use EventSauce\EventSourcing\EventConsumption\EventConsumer;
#[EnableAcl]
final class FooHandler extends EventConsumer
{
#[AsEventSauceMessageHandler(
handles: FooEvent::class // If you use a translator, "handles" must be configured.
)]
public function onFooCreated(BarEvent $barEvent): void
{
// ...
}
}Example of manually registration acl consumer (or dispatcher)
(without attribute and autoconfiguration)
services:
#...
App\Consumer\FooConsumer:
tags:
-
name: andreo.eventsauce.acl
message_filter_strategy_before_translate: match_all # or match_any
message_filter_strategy_after_translate: match_all # or match_anyuse EventSauce\EventSourcing\AntiCorruptionLayer\MessageTranslator;
use Andreo\EventSauceBundle\Attribute\AsMessageTranslator;
use EventSauce\EventSourcing\Message;
#[AsMessageTranslator]
final readonly class FooMessageTranslator implements MessageTranslator
{
public function translateMessage(Message $message): Message
{
assert($message->payload() instanceof FooEvent);
// ...
return new Message(new BarEvent());
}
}Example of manually registration message filter
(without attribute and autoconfiguration)
services:
#...
App\Acl\FooMessageTranslator:
tags:
-
name: andreo.eventsauce.acl.message_translator
priority: 0
owners: []Message filter strategies:
match_all - all filters passed a condition
match_any - any filter passed a condition
Message Filter
use EventSauce\EventSourcing\AntiCorruptionLayer\MessageFilter;
use Andreo\EventSauceBundle\Attribute\AsMessageFilter;
use Andreo\EventSauceBundle\Enum\MessageFilterTrigger;
#[AsMessageFilter(MessageFilterTrigger::BEFORE_TRANSLATE)] // or after AFTER_TRANSLATE
final readonly class FooMessageFilter implements MessageFilter
{
public function allows(Message $message): bool
{
}
}Example of manually registration message filter
(without attribute and autoconfiguration)
services:
#...
App\Acl\FooMessageFilter:
tags:
-
name: andreo.eventsauce.acl.message_filter
trigger: before_translate # or after_translate
priority: 0
owners: []For example, we use Translator, but Filter works the same
use EventSauce\EventSourcing\AntiCorruptionLayer\MessageTranslator;
use Andreo\EventSauceBundle\Attribute\AsMessageTranslator;
use EventSauce\EventSourcing\MessageConsumer;
use EventSauce\EventSourcing\MessageDispatcher;
// Translator will be using through all dispatchers as MessageDispatcher::class (or consumers as MessageConsumer::class)
// Single FooConsumer (or single FooDispatcher) uses translator also
#[AsMessageTranslator(owners: [MessageDispatcher::class, FooConsumer::class])]
final readonly class FooMessageTranslator implements MessageTranslator
{
public function translateMessage(Message $message): Message
{
}
}andreo_event_sauce:
# ...
event_dispatcher:
enabled: false
message_outbox:
enabled: false
table_name: event_message_outbox # it will be used if the outbox config has doctrine repository
relay_id: event_dispatcher_relay # it is used by consume outbox message commandExample of Event Dispatcher
use EventSauce\EventSourcing\EventDispatcher;
final readonly class FooHandler
{
public function __construct(
private EventDispatcher $eventDispatcher
) {
}
public function handle(): void
{
$this->eventDispatcher->dispatch(
new FooEvent()
);
}
}andreo_event_sauce:
#...
upcaster:
enabled: false
trigger: before_unserialize # or after_unserialize (on payload or on object of message)Before unserialize
use Andreo\EventSauceBundle\Attribute\AsUpcaster;
use EventSauce\EventSourcing\Upcasting\Upcaster;
#[AsUpcaster(aggregateClass: FooAggregate::class, version: 2)]
final class FooEventV2Upcaster implements Upcaster {
public function upcast(array $message): array
{
}
}Install andreo/eventsauce-upcasting
composer require andreo/eventsauce-upcastinguse EventSauce\EventSourcing\Message;
use Andreo\EventSauce\Upcasting\MessageUpcaster\MessageUpcaster;
use Andreo\EventSauce\Upcasting\MessageUpcaster\Event;
use Andreo\EventSauceBundle\Attribute\AsUpcaster;
#[AsUpcaster(aggregateClass: FooAggregate::class, version: 2)]
final class SomeEventV2Upcaster implements MessageUpcaster {
#[Event(event: FooEvent::class)]
public function upcast(Message $message): Message
{
}
}Example of manually registration (without attribute and autoconfiguration)
services:
#...
App\Upcaster\FooUpcaster:
tags:
-
name: andreo.eventsauce.upcaster
class: App\Domain\FooAggregate
version: 2andreo_event_sauce:
#...
message_decorator: trueuse Andreo\EventSauceBundle\Attribute\AsMessageDecorator;
use EventSauce\EventSourcing\Message;
use EventSauce\EventSourcing\MessageDecorator;
#[AsMessageDecorator]
final class FooDecorator implements MessageDecorator
{
public function decorate(Message $message): Message
{
}
}Example of manually registration (without attribute and autoconfiguration)
services:
#...
App\Decorator\FooDecorator:
tags:
-
name: andreo.eventsauce.message_decoratorInstall andreo/eventsauce-outbox
composer require andreo/eventsauce-outboxBase configuration
andreo_event_sauce:
#...
message_outbox:
enabled: false
repository:
doctrine:
enabled: true
table_name: message_outbox
logger: Psr\Log\LoggerInterface # default if monolog bundle has been installedConsume outbox messages
bin/console andreo:eventsauce:message-outbox:consume relay_idUseful aliases
EventSauce\BackOff\BackOffStrategy: EventSauce\BackOff\ExponentialBackOffStrategyEventSauce\MessageOutbox\RelayCommitStrategy: EventSauce\MessageOutbox\MarkMessagesConsumedOnCommitTo use:
- doctrine snapshot repository
- versioned snapshots
- conditional persist
package andreo/eventsauce-snapshotting is required
andreo_event_sauce:
#...
snapshot:
enabled: false
repository:
enabled: false
doctrine:
enabled: true
table_name: snapshot_store
versioned: false # it enables versioned repository for all aggregates with snapshots enabled
conditional: falseUseful aliases
Andreo\EventSauce\Snapshotting\Repository\Versioned\SnapshotVersionInflector: Andreo\EventSauce\Snapshotting\Repository\Versioned\InflectVersionFromReturnedTypeOfSnapshotStateCreationMethodAndreo\EventSauce\Snapshotting\Repository\Versioned\SnapshotVersionComparator: Andreo\EventSauce\Snapshotting\Repository\Versioned\EqSnapshotVersionComparatorInstall andreo/eventsauce-migration-generator
andreo_event_sauce:
#...
migration_generator:
dependency_factory: doctrine.migrations.dependency_factory # default if doctrine migrations bundle has been installedGenerate migration for foo prefix
bin/console andreo:eventsauce:doctrine-migrations:generate foo Useful aliases
EventSauce\MessageRepository\TableSchema\TableSchema: EventSauce\MessageRepository\TableSchema\DefaultTableSchemaandreo_event_sauce:
#...
aggregates:
foo: # aggregate name
class: ~ # aggregate FQCN
repository_alias: fooRepository # according to convention: $name . "Repository"
message_outbox:
enabled: false # enable message outbox for this aggregate
relay_id: foo_aggregate_relay # relay-id for run consume outbox messages command, according to convention: $name . "aggregate_relay"
dispatchers: [] # dispatcher service aliases (from config, or manually registered), if empty, messages will be sent to all dispatchers
upcaster: false # enable upcaster for this aggregate
snapshot:
conditional: # enable conditional snapshot repository for this aggregate.
enabled: false
every_n_event: # you can use this strategy, or make your own implementation
enabled: false
number: 100Repository injection
use Symfony\Component\DependencyInjection\Attribute\Target;
use EventSauce\EventSourcing\AggregateRootRepository;
final class FooHandler {
public function __construct(
#[Target('fooRepository')] private AggregateRootRepository $fooRepository
){}
}Snapshotting repository injection (if aggregate snapshot is enabled)
use Symfony\Component\DependencyInjection\Attribute\Target;
use EventSauce\EventSourcing\Snapshotting\AggregateRootRepositoryWithSnapshotting;
final class FooHandler {
public function __construct(
#[Target('fooRepository')] private AggregateRootRepositoryWithSnapshotting $fooRepository
){}
}Useful aliases
andreo.eventsauce.snapshot.conditional_strategy.$aggregateName: Andreo\EventSauce\Snapshotting\Repository\Conditional\ConditionalSnapshotStrategyEventSauce\EventSourcing\Serialization\PayloadSerializer: EventSauce\EventSourcing\Serialization\ConstructingPayloadSerializerEventSauce\EventSourcing\Serialization\MessageSerializer: EventSauce\EventSourcing\Serialization\ConstructingMessageSerializerEventSauce\UuidEncoding\UuidEncoder: EventSauce\UuidEncoding\BinaryUuidEncoderEventSauce\EventSourcing\ClassNameInflector: EventSauce\EventSourcing\DotSeparatedSnakeCaseInflector<?php
use EventSauce\EventSourcing\AggregateRootId;
use EventSauce\EventSourcing\AggregateRootRepository;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
#[AsDecorator(decorates: 'fooRepository')]
final readonly class FooRepository implements AggregateRootRepository
{
public function __construct(private AggregateRootRepository $regularRepository)
{
}
public function retrieve(AggregateRootId $aggregateRootId): object
{
return $this->regularRepository->retrieve($aggregateRootId);
}
public function persist(object $aggregateRoot): void
{
// ...
}
public function persistEvents(AggregateRootId $aggregateRootId, int $aggregateRootVersion, object ...$events): void
{
// ...
}
}