Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Event Sourcing Bundle Laravel Package

dddominio/event-sourcing-bundle

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Install the Bundle

    composer require dddominio/event-sourcing-bundle "1.0@dev"
    

    Ensure your composer.json includes "minimum-stability": "dev" if using dev versions.

  2. Enable the Bundle Add to config/bundles.php (Symfony 4.3+):

    return [
        // ...
        DDDominio\EventSourcingBundle\DDDominioEventSourcingBundle::class => ['all' => true],
    ];
    
  3. Configure the Bundle Create config/packages/dddominio_event_sourcing.yaml:

    dddominio_event_sourcing:
        event_store: 'doctrine' # or 'in_memory' for testing
        event_store:
            doctrine:
                connection: default
                entity_manager: default
    
  4. First Use Case: Publishing an Event Define an event class (e.g., src/Event/UserRegistered.php):

    namespace App\Event;
    
    use DDDominio\EventSourcing\Event;
    
    class UserRegistered extends Event
    {
        public function __construct(public string $userId, public string $email) {}
    }
    

    Publish it in a command or service:

    use DDDominio\EventSourcingBundle\EventPublisher;
    
    class UserRegistrationCommand
    {
        public function __construct(private EventPublisher $publisher) {}
    
        public function handle(string $userId, string $email)
        {
            $this->publisher->publish(new UserRegistered($userId, $email));
        }
    }
    

Implementation Patterns

Core Workflows

  1. Event Sourcing with Aggregates Define an aggregate root (e.g., src/Domain/UserAggregate.php):

    namespace App\Domain;
    
    use DDDominio\EventSourcing\AggregateRoot;
    use App\Event\UserRegistered;
    
    class UserAggregate extends AggregateRoot
    {
        public function register(string $email)
        {
            $this->recordThat(new UserRegistered($this->id, $email));
        }
    }
    

    Replay events to reconstruct state:

    $aggregate = $eventStore->retrieve(AggregateRoot::class, $userId);
    
  2. Event Listeners Subscribe to events in config/packages/dddominio_event_sourcing.yaml:

    dddominio_event_sourcing:
        listeners:
            App\EventListener\SendWelcomeEmail: ['App\Event\UserRegistered']
    

    Implement the listener:

    namespace App\EventListener;
    
    use App\Event\UserRegistered;
    use DDDominio\EventSourcingBundle\EventListener\EventListenerInterface;
    
    class SendWelcomeEmail implements EventListenerInterface
    {
        public function handle(UserRegistered $event): void
        {
            // Send email logic
        }
    }
    
  3. Command Handling Use Symfony’s messenger or a custom command bus:

    namespace App\Command;
    
    use DDDominio\EventSourcingBundle\CommandBus;
    
    class RegisterUserCommand
    {
        public function __construct(
            private CommandBus $bus,
            private UserAggregate $aggregate
        ) {}
    
        public function handle(string $userId, string $email)
        {
            $this->bus->dispatch(
                new RegisterUser($userId, $email),
                $this->aggregate
            );
        }
    }
    
  4. Projection (Read Models) Create a projection to materialize events into a read model:

    namespace App\Projection;
    
    use App\Event\UserRegistered;
    use DDDominio\EventSourcingBundle\Projection\ProjectionInterface;
    
    class UserProjection implements ProjectionInterface
    {
        public function __invoke(UserRegistered $event, User $user): void
        {
            $user->email = $event->email;
            $user->save();
        }
    }
    

    Register in config:

    dddominio_event_sourcing:
        projections:
            App\Projection\UserProjection: ['App\Event\UserRegistered']
    

Gotchas and Tips

Pitfalls

  1. Event Store Configuration

    • Doctrine ORM: Ensure your event entity (e.g., EventRecord) has a RecordedOn timestamp and is mapped to a table with id, aggregate_id, aggregate_type, event_name, payload, and version.
    • In-Memory Store: Only use for testing; events are lost on restart.
  2. Aggregate Loading

    • Always use retrieve() with the exact aggregate type and ID. Mixing types/IDs causes AggregateNotFoundException.
    • Version conflicts: If an aggregate is loaded with version N but the store has N+1, the bundle throws ConcurrencyException. Handle retries in your application logic.
  3. Event Serialization

    • Events must be serializable. Use __serialize()/__unserialize() or implement JsonSerializable for complex objects.
    • Avoid circular references in event payloads.
  4. Listener Ordering

    • Listeners are invoked in the order they are registered in the config. Use priority keys if order matters:
      listeners:
          App\EventListener\HighPriority: ['App\Event\UserRegistered', 100]
          App\EventListener\LowPriority:  ['App\Event\UserRegistered', -100]
      

Debugging

  1. Event Store Inspection Query the event_record table directly to verify events:

    SELECT * FROM event_record WHERE aggregate_id = 'user-123' ORDER BY recorded_on;
    

    For in-memory store, dump the store’s events:

    $store = $container->get('dddominio_event_sourcing.event_store');
    dump($store->getEventsFor('user-123'));
    
  2. Aggregate Reconstruction If an aggregate fails to load, check:

    • The aggregate_type in the event store matches the class name.
    • No unhandled exceptions in event handlers during replay.
  3. Performance

    • Batch projections for large event streams:
      $projection->project($eventStore->getEventsFor($aggregateId), $readModel);
      
    • Use Doctrine’s DQL or QueryBuilder for projections to avoid loading all events.

Extension Points

  1. Custom Event Stores Implement DDDominio\EventSourcing\EventStoreInterface for non-Doctrine stores (e.g., MongoDB, Redis):

    class RedisEventStore implements EventStoreInterface
    {
        public function appendEvents(string $aggregateId, array $events): void
        {
            // Redis logic
        }
    
        public function getEventsFor(string $aggregateId): array
        {
            // Redis logic
        }
    }
    

    Register in config:

    dddominio_event_sourcing:
        event_store: 'custom'
        event_store:
            custom: App\EventStore\RedisEventStore
    
  2. Custom Event Publishers Extend DDDominio\EventSourcingBundle\EventPublisher to add middleware (e.g., logging, validation):

    class CustomEventPublisher extends EventPublisher
    {
        public function publish(Event $event): void
        {
            $this->logEvent($event); // Custom logic
            parent::publish($event);
        }
    }
    

    Bind in services:

    services:
        DDDominio\EventSourcingBundle\EventPublisher: '@App\EventPublisher\CustomEventPublisher'
    
  3. Event Metadata Attach metadata to events (e.g., RecordedOn, UserId):

    $event = new UserRegistered($userId, $email);
    $event->setMetadata(['user_id' => $userId, 'source' => 'api']);
    

    Access in listeners:

    $metadata = $event->getMetadata()['user_id'];
    
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
emuniq/filament-browser-notifications
syriable/filament-translator
hungnm28/livewire-form
wenprise/eloquent
crudly/encrypted
fadion/bouncy
cuci/prototurk-sdk
gos/pubsub-router-bundle
cuci/prototurk-sdk-symfony
clementtalleu/easyadmin-markdown-bundle
codeflextech/permission-manager
karnoweb/livewire-datepicker
sayedenam/sayed-dashboard
milito/query-filter
apiboxsym/user-bundle
apiboxsym/health-check-bundle
jayeshmepani/jpl-moshier-ephemeris-php
elnasnato/laraliveui
labrodev/rest-sdk
sampaui/sampaui