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],
];For local Docker-based verification, run make up on the host and then run make install phpstan test inside the PHP container. At the moment, some legacy development packages may need to be removed from composer.json before that workflow works end to end, because packages such as andreo/eventsauce-aggregate, andreo/eventsauce-outbox, and andreo/eventsauce-upcasting are no longer available for Composer to install. Those have been removed in this copy of the library.
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
{
// ...
}
}