andreo/eventsauce-bundle

Symfony bundle for EventSauce.

Installs: 1 656

Dependents: 0

Suggesters: 0

Security: 0

Stars: 16

Watchers: 3

Forks: 3

Open Issues: 0

Type:symfony-bundle

3.0 2023-03-28 18:15 UTC

This package is auto-updated.

Last update: 2024-04-11 06:46:44 UTC


README

68747470733a2f2f6576656e7473617563652e696f2f7374617469632f6c6f676f2e737667

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

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

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

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

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

Example api using this bundle

Repository