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

Laravel Event Projector Laravel Package

spatie/laravel-event-projector

Deprecated in favor of spatie/laravel-event-sourcing. Entry-level event sourcing toolkit for Laravel: define aggregates, projectors, and reactors; persist domain events, build read models, and react to events for auditing and reporting-friendly apps.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Installation:

    composer require spatie/laravel-event-projector
    php artisan vendor:publish --provider="Spatie\EventProjector\EventProjectorServiceProvider"
    

    Publish the config and migrations to set up the database tables for events and aggregates.

  2. Define an Aggregate: Create a class extending Spatie\EventProjector\Aggregate:

    namespace App\Aggregates;
    
    use Spatie\EventProjector\Aggregate;
    
    class Order extends Aggregate
    {
        public function placeOrder()
        {
            $this->recordThat(new OrderPlaced($this->id, $this->userId));
        }
    }
    
  3. Define an Event: Create an event class extending Spatie\EventProjector\Event:

    namespace App\Events;
    
    use Spatie\EventProjector\Event;
    
    class OrderPlaced extends Event
    {
        public $userId;
    
        public function __construct($orderId, $userId)
        {
            $this->orderId = $orderId;
            $this->userId = $userId;
        }
    }
    
  4. Record an Event:

    $order = new Order($orderId, $userId);
    $order->placeOrder();
    $order->persist();
    
  5. First Use Case: Trigger an event and persist it to the database:

    event(new OrderPlaced(1, 1));
    // or via aggregate
    $order = Order::retrieve($orderId);
    $order->placeOrder();
    $order->persist();
    

Implementation Patterns

Core Workflows

Event Sourcing with Aggregates

  • Aggregate Lifecycle:

    • Use Aggregate::retrieve($id) to load an aggregate by its ID.
    • Record events using $aggregate->recordThat(new Event()).
    • Persist changes with $aggregate->persist().
    • Replay events with $aggregate->replay() to reconstruct state.
  • Example Workflow:

    // Create and persist
    $order = new Order($orderId, $userId);
    $order->placeOrder();
    $order->persist();
    
    // Load and replay
    $order = Order::retrieve($orderId);
    $order->replay();
    

Projectors for Read Models

  • Define a Projector:

    namespace App\Projectors;
    
    use Spatie\EventProjector\Projector;
    use App\Events\OrderPlaced;
    
    class OrderProjector extends Projector
    {
        public function handleOrderPlaced(OrderPlaced $event)
        {
            // Update read model (e.g., database, cache)
            \DB::table('order_read_models')->updateOrCreate(
                ['order_id' => $event->orderId],
                ['user_id' => $event->userId, 'status' => 'placed']
            );
        }
    }
    
  • Register Projectors: In config/event-projector.php, list projectors under the projectors key:

    'projectors' => [
        \App\Projectors\OrderProjector::class,
    ],
    
  • Run Projectors:

    php artisan event-projector:project
    

    Or manually:

    $projector = app(\Spatie\EventProjector\Projector::class);
    $projector->project();
    

Reactors for Side Effects

  • Define a Reactor:

    namespace App\Reactors;
    
    use Spatie\EventProjector\Reactor;
    use App\Events\OrderPlaced;
    
    class OrderReactor extends Reactor
    {
        public function handleOrderPlaced(OrderPlaced $event)
        {
            // Trigger side effects (e.g., send email, update inventory)
            \Log::info("Order {$event->orderId} placed by user {$event->userId}");
        }
    }
    
  • Register Reactors: In config/event-projector.php, list reactors under the reactors key:

    'reactors' => [
        \App\Reactors\OrderReactor::class,
    ],
    
  • Run Reactors:

    php artisan event-projector:react
    

Event Replaying

  • Replay All Events:

    php artisan event-projector:replay
    

    Or for a specific aggregate:

    $aggregate = Order::retrieve($orderId);
    $aggregate->replay();
    
  • Replay Events Up to a Point: Use the --until option:

    php artisan event-projector:replay --until=2023-01-01
    

Integration Tips

Combining with Laravel Queues

  • Dispatch events to queues for async processing:

    event(new OrderPlaced($orderId, $userId))->onQueue('events');
    
  • Process queued events with reactors:

    // In a job handler
    $reactor = app(\Spatie\EventProjector\Reactor::class);
    $reactor->react($event);
    

Testing

  • Mock Projectors/Reactors: Use partial mocks to test event handling:

    $projector = Mockery::mock(\App\Projectors\OrderProjector::class);
    $projector->shouldReceive('handleOrderPlaced')->once();
    
  • Test Aggregates: Replay events in tests to verify state:

    $order = new Order($orderId, $userId);
    $order->replay();
    $this->assertEquals('placed', $order->status);
    

Performance

  • Batch Processing: Use chunking for large event sets:

    \DB::table('events')->cursor()->chunk(100, function ($events) {
        foreach ($events as $event) {
            $projector->handle($event);
        }
    });
    
  • Indexing: Ensure occurred_at is indexed in the events table for faster queries.


Gotchas and Tips

Pitfalls

Event Ordering

  • Issue: Events must be replayed in chronological order.
    • Fix: Always use occurred_at timestamps and ensure no out-of-order inserts.

Aggregate Loading

  • Issue: Loading an aggregate without replaying events leaves it in an inconsistent state.
    • Fix: Always call replay() after retrieve():
      $order = Order::retrieve($orderId);
      $order->replay(); // Critical step!
      

Projector/Reactor Registration

  • Issue: Forgetting to register projectors/reactors in config/event-projector.php.
    • Fix: Verify the config file includes all custom classes:
      'projectors' => [
          \App\Projectors\OrderProjector::class,
      ],
      

Event Serialization

  • Issue: Custom events not serializable (e.g., containing non-JSON-serializable objects).
    • Fix: Implement __serialize() and __unserialize() or use JsonSerializable:
      class OrderPlaced implements JsonSerializable
      {
          public function jsonSerialize()
          {
              return [
                  'orderId' => $this->orderId,
                  'userId' => $this->userId,
              ];
          }
      }
      

Database Transactions

  • Issue: Long-running projector/reactor batches may cause timeouts.
    • Fix: Use smaller batches or chunk processing:
      \DB::transaction(function () {
          $events = \App\Events\OrderPlaced::chunk(50);
          // Process in transaction
      });
      

Debugging

Event Not Firing

  • Check:
    • Ensure the event is dispatched (e.g., event(new OrderPlaced(...))).
    • Verify the event class extends Spatie\EventProjector\Event.
    • Check the events table for the recorded event.

Projector Not Updating Read Model

  • Check:
    • Confirm the projector is registered in config/event-projector.php.
    • Verify the handle* method matches the event class name.
    • Run php artisan event-projector:project manually to test.

Reactor Side Effects Missing

  • Check:
    • Ensure the reactor is registered in config/event-projector.php.
    • Test reactors manually:
      $reactor = app(\Spatie\EventProjector\Reactor::class);
      $reactor->react(new OrderPlaced(1, 1));
      

Aggregate State Mismatch

  • Check:
    • Replay events step-by-step in tinker:
      php artisan tinker
      >>> $order = App\Aggregates\Order::retrieve(1);
      >>> $order->replay();
      >>> $order->status // Verify state
      

Configuration Quirks

Custom Event Table

  • Override Default Table: In `config/event
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.
davejamesmiller/laravel-breadcrumbs
artisanry/parsedown
christhompsontldr/phpsdk
enqueue/dsn
bunny/bunny
enqueue/test
enqueue/null
enqueue/amqp-tools
milesj/emojibase
bower-asset/punycode
bower-asset/inputmask
bower-asset/jquery
bower-asset/yii2-pjax
laravel/nova
spatie/laravel-mailcoach
spatie/laravel-superseeder
laravel/liferaft
nst/json-test-suite
danielmiessler/sec-lists
jackalope/jackalope-transport