Weave 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

Eventsauce Bundle Laravel Package

jphooiveld/eventsauce-bundle

A Symfony bundle that integrates EventSauce event sourcing into your application, providing service configuration for aggregates, repositories, message dispatching, and event storage so you can build domain-driven, event-sourced systems with less boilerplate.

View on GitHub
Deep Wiki
Context7

Getting Started

First reality check: this is a Symfony bundle, not a Laravel package. composer require jphooiveld/eventsauce-bundle inside a Laravel app will pull Symfony's DI/HttpKernel components and the bundle will never boot. For a Laravel codebase you leverage this repo in one of two ways:

  1. As a blueprint (the normal case): install eventsauce/eventsauce directly and copy the bundle's service definitions into a ServiceProvider. The bundle is small — its value is showing you which EventSauce objects need wiring and in what order.
  2. On a Symfony service in the same estate: composer require jphooiveld/eventsauce-bundle, register the bundle in config/bundles.php (Flex does it), then configure under config/packages/. Confirm the bundle FQCN with composer show jphooiveld/eventsauce-bundle before hand-editing bundles.php.

Where to look first (in this order, ~20 minutes):

  • README.md — config keys and the consumer tag name.
  • DependencyInjection/Configuration.php — the actual config tree; this tells you exactly what is tunable versus hardcoded.
  • DependencyInjection/*Extension.php — default service IDs and aliases.
  • Resources/config/* — the wiring you are about to reimplement in Laravel.
  • Any CompilerPass — this is where tagged message consumers get collected; you need the tag name.

Minimal Laravel service provider (mirrors what the bundle does, EventSauce 1.x API):

$this->app->singleton(MessageSerializer::class, fn () => new ConstructingMessageSerializer());
$this->app->singleton(MessageRepository::class, fn ($app) => new DbalMessageRepository(
    $app->make(Connection::class)
));
$this->app->tag([SendInvoice::class, UpdateOrderList::class], 'eventsauce.consumer');
$this->app->singleton(MessageDispatcher::class, fn ($app) => new SynchronousMessageDispatcher(
    ...iterator_to_array($app->tagged('eventsauce.consumer'))
));
$this->app->bind(OrderRepository::class, fn ($app) => new AggregateRootRepository(
    Order::class,
    $app->make(MessageRepository::class),
    $app->make(MessageDispatcher::class),
    $app->make(MessageSerializer::class),
));

First use case: one aggregate (Order) with AggregateRootBehaviour, one command handler that calls retrieve() / persist(), an in-memory MessageRepository in tests, DBAL in production, and one projection consumer that writes a read model. Prove the loop end-to-end before adding a second aggregate — every later decision (serializer, outbox, snapshots) hangs off this loop.

Implementation Patterns

One repository binding per aggregate root. Don't try to make a single generic repository; bind OrderRepository, CustomerRepository, etc. as thin aliases around AggregateRootRepository, each constructed with its own aggregate class name. This keeps the call sites honest ($orders->retrieve($id) returns an Order, not an AggregateRoot) and makes static analysis useful.

Command → handler → repository is the whole workflow. persist() stores messages and dispatches them, in that order. Everything downstream — projections, side effects, notifications — is a MessageConsumer reacting to the dispatch. Resist the urge to call consumers directly from controllers.

Projections as tagged consumers. Register each projector under the same tag and let the dispatcher fan out. In Laravel, prefer queued consumers for anything slow:

final class UpdateOrderList implements MessageConsumer
{
    public function __invoke(Message $message): void
    {
        match (true) {
            $message->event() instanceof OrderPlaced => dispatch(new ProjectOrderPlaced($message)),
            default => null,
        };
    }
}

Outbox pattern is the integration point with Laravel's transaction model. The bundle ships a synchronous dispatcher, which means a throwing consumer leaves you with a committed write and a broken projection. In Laravel this is trivially fixable:

DB::transaction(function () use ($order) {
    $messages = $this->orders->persist($order); // or capture via a decorator
    // write same messages to a `event_outbox` table inside the transaction
});
// after commit: a job drains the outbox and re-dispatches to consumers

If you'd rather not capture return values, wrap your MessageRepository in a decorator that writes to the outbox table, and point MessageDispatcher at the drainer only.

Serializer is where schema evolution lives. ConstructingMessageSerializer round-trips through toPayload() / fromPayload() on your event classes. For day-to-day safety, wrap it:

new UpcastingMessageSerializer(
    new ConstructingMessageSerializer(),
    new DelegatingUpcaster([new EventTypeMigrated('order_placed.v1', 'order_placed.v2', UpgradePayload::class)])
);

Snapshots for long-lived aggregates. Wrap the repository in SnapshotAggregateRootRepository with a SnapshotRepository once an aggregate routinely replays more than a few hundred events (order timelines, ledger accounts). Persist snapshots to Redis or a dedicated table; they are disposable, so never treat them as the source of truth.

Testing pattern: swap MessageRepository for InMemoryMessageRepository in the test container and assert on the messages produced by a command. This keeps aggregate tests fast and free of DB setup, and it's the single biggest ergonomic win of following the bundle's structure.

Gotchas and Tips

  • Version pinning is the real risk. Last release was November 2019, so it targets EventSauce 1.x and Symfony 4/5. EventSauce 2.x changed AggregateRootRepository from a concrete class to an interface (with EventSourcedAggregateRootRepository as the implementation), among other signature changes. If you're on PHP 8.2+, expect --ignore-platform-reqs gymnastics on a Symfony stack — another reason to mine the patterns rather than install.
  • Renaming an event class breaks replay. The serialized payload embeds the event type. Moving a namespace or renaming a class silently orphans stored messages. Lock the type string via a delegating serializer / event_type alias map from day one, and treat event class names as public API.
  • Concurrency exceptions belong at the boundary. AggregateRootBehaviour enforces expected version on write; concurrent commands throw. Catch in the command handler and retry a bounded number of times — don't let it bubble to the controller as a 500.
  • Aggregate ID classes must match exactly. Mixing UuidAggregateRootId and a custom implementation, or changing the ID class later, will fail deserialization on replay. Pick one and stick to it.
  • Config key naming is inconsistent. Symfony bundles of this vintage variously use eventsauce, event_sauce, or a nested event_sourcing block. Read Configuration.php rather than trusting the README, and note that service references are FQCN strings — typos there only surface at container compile time, which in Laravel means "silently resolves nothing" until you call $app->make().
  • DBAL repository needs a migration. The schema isn't auto-created. Write a Laravel migration for the event_store table (aggregate_root_id, aggregate_root_version, payload JSON) before your first persist(), and index on the ID.
  • Debugging replay is easier than debugging dispatch. When a projection looks wrong, retrieve() the aggregate and dump the message list, and eyeball the raw payload column — 90% of event-sourcing bugs are a payload shape that drifted.
  • Extension points worth knowing: a MessageDecorator chain for correlation/causation IDs and app version headers (put these on day one; retrofitting them into historical messages is impossible), the compiler-pass tag for consumers, and a custom MessageRepository if you want Eloquent rather than DBAL — the interface is three methods and small enough to implement directly.
  • Overall judgement: at 15 stars, a single release four years stale, and zero dependents, treat this as reference material for wiring EventSauce, not a dependency. The EventSauce library itself is the durable artifact; the bundle's contribution is a 200-line container configuration you can reimplement in a provider in an afternoon.
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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core