Skip to content

igefa-ebd/eventsauce-bundle

Repository files navigation

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],
];

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.

Introduction

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: UTC

Useful aliases

EventSauce\Clock\Clock: EventSauce\Clock\SystemClock

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

SynchronousMessageDispatcher

andreo_event_sauce:
  #...
  message_dispatcher: # chain of message dispatchers
    foo_dispatcher:
      type:
        sync: true
    bar_dispatcher:
      type:
        sync: true

EventConsumer

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

MessengerMessageDispatcher

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'

EventConsumer

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
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
    {
    }
}
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()
        );
    }
}
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
    {
    }
}

After unserialize

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
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

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

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

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
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
    {
        // ...
    }
}

Example api using this bundle

About

Symfony bundle for EventSauce

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages