Laravelpatchlevel/laravel-event-sourcing

Event Sourcing for Laravel

The official Laravel package for patchlevel/event-sourcing. An Eloquent-like aggregate API, facades for commands, queries, repositories, and the store, Artisan commands for the projection lifecycle, and read models built with the Schema and DB facades - event sourcing that feels like Laravel.

Terminal
$ composer require patchlevel/laravel-event-sourcing

$ php artisan vendor:publish --tag patchlevel-config
$ php artisan vendor:publish --tag patchlevel-migrations
$ php artisan migrate
Facades
Eloquent-like developer experience
Artisan
Full CLI tooling included
Auto-discovery
Service provider, zero config
MIT
Open source, forever
Quickstart

Event sourcing, the Laravel way

Define events and aggregates as plain PHP, then work with them the way you work with Laravel: static load and save on the aggregate, facades for the services, and read models built with the Schema and DB facades.

Step

Define an event

Pure PHP classes with a single attribute - no base class, no framework coupling. Your domain model stays clean and testable.

app/Events/GuestIsCheckedIn.php
#[Event('hotel.guest_is_checked_in')]
final class GuestIsCheckedIn
{
    public function __construct(
        public readonly string $guestName,
    ) {}
}
Step

Model the aggregate

Extend the package's AggregateRoot and your aggregate gets static load and save helpers - the same mental model as an Eloquent model, backed by an event stream.

app/Models/Hotel.php
#[Aggregate(name: 'hotel')]
final class Hotel extends AggregateRoot
{
    #[Id]
    private Uuid $id;

    /** @var list<string> */
    private array $guests = [];

    public function checkIn(string $guestName): void
    {
        if (in_array($guestName, $this->guests, true)) {
            throw new GuestAlreadyCheckedIn($guestName);
        }

        $this->recordThat(new GuestIsCheckedIn($guestName));
    }

    #[Apply]
    protected function applyGuestIsCheckedIn(GuestIsCheckedIn $event): void
    {
        $this->guests[] = $event->guestName;
    }
}
Step

Load & save like Eloquent

No repository wiring needed in your controllers: load the aggregate by id, call a business method, save. The package replays the event history behind the scenes.

app/Http/Controllers/HotelController.php
final class HotelController
{
    public function checkIn(string $id, Request $request): JsonResponse
    {
        $hotel = Hotel::load(Uuid::fromString($id));
        $hotel->checkIn($request->json('name'));
        $hotel->save();

        return response()->json();
    }
}
Step

Project with the DB facade

Projectors build read models the Laravel way: create tables with a Schema Blueprint in #[Setup], write rows with the DB facade. Rebuild any time from the event stream.

app/Subscribers/HotelProjection.php
#[Projector('hotel')]
final class HotelProjection
{
    use SubscriberUtil;

    #[Subscribe(GuestIsCheckedIn::class)]
    public function handleGuestIsCheckedIn(Uuid $hotelId): void
    {
        DB::table($this->table())
            ->where('id', $hotelId->toString())
            ->increment('guests');
    }

    #[Setup]
    public function create(): void
    {
        Schema::create($this->table(), static function (Blueprint $table): void {
            $table->uuid('id')->primary();
            $table->string('name');
            $table->integer('guests');
        });
    }
}
Deep integration

A native Laravel feel

Not a generic library with a thin adapter. The package plugs event sourcing into the Laravel building blocks you already use every day.

Auto-Discovery

composer require and the service provider registers every binding automatically. Publish the config when you want to customize - not before.

Facades

CommandBus, QueryBus, Repository, Store, and ProjectionConnection facades give you quick access to the user-facing services - or use plain dependency injection if you prefer.

Artisan Commands

Schema management, event debugging, and the full subscription lifecycle - boot, run, replay, pause, reactivate - as php artisan commands.

Publishable Config & Migrations

config/event-sourcing.php and the event store migrations are published with vendor:publish and run through the standard php artisan migrate flow.

Works with Eloquent

Event-sourced aggregates and Eloquent models coexist. Keep CRUD where CRUD is fine and event-source the parts of your domain that need history.

Native Side Effects

Processors are plain classes - send mail with the Mail facade, dispatch notifications, or push jobs to queues whenever an event happens.
Facades & Artisan

CQRS with a Laravel API

Dispatch commands and queries through facades, reach the store and repositories the same way, and drive the whole event-sourcing lifecycle from Artisan.

  • CommandBus facade

    Dispatch commands with CommandBus::dispatch - handled by the method you marked with the #[Handle] attribute, including aggregate loading and saving.

  • QueryBus facade

    Retrieve data with QueryBus::dispatch - answered by services via the #[Answer] attribute.

  • Repository & Store facades

    Grab the repository for any aggregate with Repository::get, or load and save raw messages through the Store facade.

  • Artisan all the way

    Set up subscriptions, boot projections, and run workers with php artisan event-sourcing:* commands.

app/Http/Controllers/BookingController.php
use Patchlevel\LaravelEventSourcing\Facade\CommandBus;
use Patchlevel\LaravelEventSourcing\Facade\QueryBus;
use Patchlevel\LaravelEventSourcing\Facade\Repository;

// dispatch a command
CommandBus::dispatch(new BookHotel($hotelId, $guestName));

// ask a question
$count = QueryBus::dispatch(new HotelCountQuery());

// or talk to the repository directly
$repository = Repository::get(Hotel::class);
$hotel = $repository->load($hotelId);
Terminal
$ php artisan event-sourcing:subscription:setup
$ php artisan event-sourcing:subscription:boot
$ php artisan event-sourcing:subscription:run
Batteries included

The full power of the core library

The integration ships everything from patchlevel/event-sourcing - production features you would otherwise build yourself.

FAQ

Frequently asked questions

Everything you need to know about event sourcing in Laravel with patchlevel.

How do I add event sourcing to a Laravel application?

Run composer require patchlevel/laravel-event-sourcing. The service provider is auto-discovered. Publish the config with php artisan vendor:publish --tag patchlevel-config, publish and run the migrations, and add the EventSourcingMiddleware in bootstrap/app.php - then you are ready to define your first aggregate.

Does the package feel like Laravel?

Yes. Aggregates extend an AggregateRoot base class with static load and save helpers similar to Eloquent models, facades like Repository, Store, CommandBus, and QueryBus give quick access to services, and Artisan picks up all CLI commands automatically.

Can I use event sourcing together with Eloquent?

Absolutely. Event-sourced aggregates and Eloquent models coexist in the same application. Projections typically build read models with the Schema and DB facades or with Eloquent models, so the read side stays plain Laravel.

Which databases does the event store support?

The event store is built on Doctrine DBAL and supports PostgreSQL, MySQL, MariaDB, and SQLite out of the box. Custom stores for other technologies can be implemented against clean interfaces.

How do projections work in Laravel?

Projectors subscribe to events with the #[Subscribe] attribute and write read models using the DB facade, the Schema builder, or Eloquent. The subscription engine manages their lifecycle - boot, replay, rebuild, retry - via Artisan commands.

Is patchlevel/laravel-event-sourcing production ready?

The package is the official Laravel integration of patchlevel/event-sourcing, which has over 300,000 Packagist installs. It is MIT licensed, fully typed, and includes production features like GDPR-compliant crypto-shredding, snapshots, and a managed subscription lifecycle.

Start event sourcing in Laravel

Install the package and build your first aggregate, event, and projection in minutes - the getting-started guide walks you through it.