andreo / eventsauce-bundle
Symfony bundle for EventSauce.
Installs: 3 402
Dependents: 0
Suggesters: 0
Security: 0
Stars: 17
Watchers: 3
Forks: 3
Open Issues: 0
Type:symfony-bundle
Requires
- php: >=8.2
- eventsauce/eventsauce: ^3.0
- eventsauce/message-repository-for-doctrine: ^0.4.1
- symfony/config: ^6.2
- symfony/dependency-injection: ^6.2
- symfony/http-kernel: ^6.2
Requires (Dev)
- andreo/eventsauce-aggregate: dev-main
- andreo/eventsauce-messenger: dev-main
- andreo/eventsauce-migration-generator: dev-main
- andreo/eventsauce-outbox: dev-main
- andreo/eventsauce-snapshotting: dev-main as 3.2
- andreo/eventsauce-upcasting: dev-main
- doctrine/doctrine-migrations-bundle: ^3.0
- matthiasnoback/symfony-dependency-injection-test: ^4.3
- phpstan/phpstan: ^1.9
- phpunit/phpunit: ^9.4
- roave/security-advisories: dev-latest
- symfony/monolog-bundle: 3.x-dev
Suggests
- andreo/eventsauce-aggregate: Extended aggregate components of the EventSauce.
- andreo/eventsauce-messenger: Integration symfony messenger.
- andreo/eventsauce-migration-generator: Generates doctrine migrations per aggregate.
- andreo/eventsauce-outbox: Extended message outbox components.
- andreo/eventsauce-snapshotting: Extended snapshot components.
- andreo/eventsauce-upcasting: Extended upcasting components.
This package is auto-updated.
Last update: 2024-12-11 08:14:14 UTC
README
EventSauceBundle 3.0
Official documentation of eventsauce
Supports
- 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
Previous versions
Requirements
- PHP >=8.2
- Symfony ^6.2
Installation
composer require andreo/eventsauce-bundle
// config/bundles.php return [ Andreo\EventSauceBundle\AndreoEventSauceBundle::class => ['all' => true], ];
Introduction
Below configs presents default values and some example values.
Note that most of default config values do not need to configure.
Clock
andreo_event_sauce: clock: timezone: UTC
Useful aliases
EventSauce\Clock\Clock: EventSauce\Clock\SystemClock
Message Storage
Doctrine 3
andreo_event_sauce: #... message_storage: repository: doctrine_3: enabled: true json_encode_flags: [] connection: doctrine.dbal.default_connection table_name: event_store
Require
- doctrine/dbal
Message dispatcher
andreo_event_sauce: #... message_dispatcher: # chain of message dispatchers foo_dispatcher: type: sync: true bar_dispatcher: type: sync: true
use 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_consumer
Dispatching with Symfony messenger
Install andreo/eventsauce-messenger
composer require andreo/eventsauce-messenger
andreo_event_sauce: #... message_dispatcher: # chain of message dispatchers foo_dispatcher: type: messenger: bus: event_bus # bus alias from messenger config
It registers alias of handle event sauce message middleware:
$busAlias.handle_eventsauce_message: Andreo\EventSauce\Messenger\Middleware\HandleEventSauceMessageMiddleware
Update 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\InflectHandlerMethodsFromType
Message dispatcher tag (for manually registration of dispatchers, if you will want)
andreo.eventsauce.message_dispatcher
Anti-Corruption Layer
andreo_event_sauce: #... acl: true
Enable 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_any
Message translator
use 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
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: []
Owners of message translator or filters
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 { } }
Event Dispatcher
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 command
Example 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() ); } }
Upcaster
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-upcasting
use 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: 2
Message decorator
andreo_event_sauce: #... message_decorator: true
use 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_decorator
Message Outbox
Install andreo/eventsauce-outbox
composer require andreo/eventsauce-outbox
Base 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 installed
Consume outbox messages
bin/console andreo:eventsauce:message-outbox:consume relay_id
Useful aliases
EventSauce\BackOff\BackOffStrategy: EventSauce\BackOff\ExponentialBackOffStrategy
EventSauce\MessageOutbox\RelayCommitStrategy: EventSauce\MessageOutbox\MarkMessagesConsumedOnCommit
Snapshotting
To 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: false
Useful aliases
Andreo\EventSauce\Snapshotting\Repository\Versioned\SnapshotVersionInflector: Andreo\EventSauce\Snapshotting\Repository\Versioned\InflectVersionFromReturnedTypeOfSnapshotStateCreationMethod
Andreo\EventSauce\Snapshotting\Repository\Versioned\SnapshotVersionComparator: Andreo\EventSauce\Snapshotting\Repository\Versioned\EqSnapshotVersionComparator
Migration generator
Install andreo/eventsauce-migration-generator
andreo_event_sauce: #... migration_generator: dependency_factory: doctrine.migrations.dependency_factory # default if doctrine migrations bundle has been installed
Generate migration for foo prefix
bin/console andreo:eventsauce:doctrine-migrations:generate foo
Useful aliases
EventSauce\MessageRepository\TableSchema\TableSchema: EventSauce\MessageRepository\TableSchema\DefaultTableSchema
Aggregates
andreo_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: 100
Repository 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\ConditionalSnapshotStrategy
EventSauce\EventSourcing\Serialization\PayloadSerializer: EventSauce\EventSourcing\Serialization\ConstructingPayloadSerializer
EventSauce\EventSourcing\Serialization\MessageSerializer: EventSauce\EventSourcing\Serialization\ConstructingMessageSerializer
EventSauce\UuidEncoding\UuidEncoder: EventSauce\UuidEncoding\BinaryUuidEncoder
EventSauce\EventSourcing\ClassNameInflector: EventSauce\EventSourcing\DotSeparatedSnakeCaseInflector
Other tips
Decorating aggregate root repository
<?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 { // ... } }