spatie/laravel-event-sourcing

repository·main·Indexed 21 days ago

https://github.com/spatie/laravel-event-sourcing

A Laravel package for implementing event sourcing, providing tools to manage aggregates, projectors, and reactors. It features a command bus with automatic aggregate mapping, aggregate partials for managing complex roots, and event queries for in-memory projections. This package supersedes laravel-event-projector.

Tokens
28.1K
Snippets
93
Records
121
Agent score
71%

What's inside spatie/laravel-event-sourcing

  1. Overview of Laravel Event Sourcing package capabilities

    main

    The spatie/laravel-event-sourcing package serves as an entry point for implementing event sourcing within Laravel applications. It provides the necessary infrastructure to set up and manage the core components of an event-sourced system:

    • Aggregates: The domain objects that handle commands and record events.
    • Projectors: Components that listen to events to build read models (projections).
    • Reactors: Components that listen to events to trigger side effects (like sending emails or calling external APIs).

    Event sourcing is particularly useful if your application requires auditing (understanding why a state changed), needs to make decisions based on historical data, or requires flexible reporting capabilities for data that isn't yet fully defined.

  2. Overview of Laravel Event Sourcing

    main

    The spatie/laravel-event-sourcing package provides a framework for implementing event sourcing within Laravel applications. It facilitates the management of aggregates, projectors, and reactors.

    When to use event sourcing:

    • When your application needs to make decisions based on historical data.
    • When you have strict auditing requirements (understanding why a state changed is as important as the state itself).
    • When you anticipate future reporting needs but are currently unsure of the specific data requirements.
  3. What is a reactor and when to use one

    main

    A reactor is a class used to handle side effects (e.g., sending emails, notifications, or making API calls) in response to events.

    Key Characteristics

    • Side Effect Isolation: Unlike projectors, reactors are not called when replaying events. They only trigger when the original event occurs in real-time. This prevents side effects from being duplicated during a replay.
    • Queueing: Because side effects (like API calls) can be slow, it is highly recommended that all reactors implement the Illuminate\Contracts\Queue\ShouldQueue interface. Implementing this marker interface tells the package to handle the reactor via a queued job rather than executing it synchronously.
  4. What is a reactor and how does it differ from a projector?

    main

    A reactor is a class that listens for incoming events.

    Key distinction: Unlike projectors, reactors will not be called when events are replayed. They are only triggered when the original event fires in real-time. This makes them suitable for side effects like sending emails or triggering external integrations that should not be repeated during a replay.

    Reactors can be created using the Artisan command:

    php artisan make:reactor BigAmountAddedReactor
  5. What is event sourcing and how does it work?

    main

    Event sourcing is a pattern where you store all changes to your application state as a sequence of events, rather than just storing the current state in a database. This provides a complete history of how the application reached its current state.

    In this package, the system is built around two core concepts:

    1. Aggregates: These are used to validate whether a new event is allowed to be written and to make decisions based on the application's past state.
    2. Projectors: These are used to transform newly written events into a format that is useful for consumption (e.g., updating a read-model database or a search index).

    Example: A Banking System

    • Traditional approach: You store a single balance field in an accounts table. You lose the history of how that balance was reached.
    • Event sourcing approach: You store every transaction (e.g., MoneyDeposited, MoneyWithdrawn) as an event. The account balance is not a standalone field, but a value calculated by replaying those stored transactions.
  6. How projectors transform events

    main

    In an event-sourced system, instead of directly updating database records (which causes loss of historical data), you record every change as an event in a stored_events table.

    A projector is a class that listens to these events and transforms them into a specific format or database table that is optimized for your application's needs (often called a 'projection').

    Key characteristics of projectors:

    • Data Transformation: They can take granular events (e.g., MoneyAdded, MoneySubtracted) and build a high-level state (e.g., an Accounts table).
    • Replayability: If you need new information (e.g., a report on which accounts have the most transactions), you can create a new projector that reads all existing historical events to build a new projection without losing any data.
    • Decoupling: The application logic fires events, and projectors handle the side effects of updating read models.
  7. Manage aggregate state with apply methods

    main

    When an aggregate is retrieved, all its past events are replayed. To rebuild the internal state of the aggregate (e.g., a balance or a counter), you must implement apply methods following the naming convention apply<EventClassName>.

    These methods are automatically called by the package during the event replay process, allowing you to update private instance variables based on the data contained within the events.

    class AccountAggregate extends AggregateRoot
    {
        private int $balance = 0;
    
        public function applyMoneyAdded(MoneyAdded $event)
        {
            $this->balance += $event->amount;
        }
    
        public function applyMoneySubtracted(MoneySubtracted $event)
        {
            $this->balance -= $event->amount;
        }
    }
  8. Use EventHandlerFailedHandlingEvent to respond to failures

    main

    When catch_exceptions is set to true, any exception thrown by a projector or reactor triggers the Spatie\EventSourcing\Events\EventHandlerFailedHandlingEvent. You can listen for this event to monitor or log failures. The event provides access to the following properties:

    • eventHandler: The specific projector or reactor instance that failed.
    • storedEvent: The Spatie\EventSourcing\Models\StoredEvent instance that was being processed.
    • exception: The actual exception that was thrown.
  9. Implement a command bus with automatic aggregate mapping

    main

    The package provides a command bus implementation that automatically maps command objects to their corresponding aggregate roots or partials. This reduces boilerplate by eliminating the need for manual command handlers.

    To use this feature:

    1. Create a command class.
    2. Use the #[HandledBy] attribute to specify the target aggregate root or partial.
    3. If targeting an aggregate root, ensure at least one property is marked with the #[AggregateUuid] attribute so the bus can identify which aggregate instance to load.
    4. Add a method to the target aggregate/partial that accepts the command class as an argument.
    5. Dispatch the command using the CommandBus.
    namespace Spatie\Shop\Cart\Commands;
    
    use Spatie\Shop\Support\EventSourcing\Attributes\AggregateUuid;
    use Spatie\Shop\Support\EventSourcing\Attributes\HandledBy;
    
    #[HandledBy(CartAggregateRoot::class)]
    class AddCartItem
    {
        public function __construct(
            #[AggregateUuid] public string $cartUuid,
            public string $cartItemUuid,
            public Product $product,
            public int $amount,
        ) {
        }
    }
  10. What are Event Queries and when to use them

    main

    An Event Query is a class that represents an in-memory projection. When an event query is instantiated, it queries relevant events from the database and applies them internally to build a specific state.

    Key Characteristics

    • Read-Only: Event queries should only ever be used to read data and must never result in changes to the system state.
    • In-Memory Projections: They build state by iterating over events and applying them to internal properties.
    • Data Set Limitations: Event queries are only viable for limited data sets. Querying over periods containing millions of events can cause performance issues. They are best suited for smaller reports or specific time windows (e.g., a month or two).
    use Spatie\EventSourcing\EventHandlers\Projectors\EventQuery;
    
    class MyQuery extends EventQuery
    {
        // Implementation details...
    }
  11. Customize the stored snapshot state with getState and useState

    main

    By default, the package uses Reflection to store all public properties in the snapshot. If you need to control exactly what data is persisted or how it is restored, you can override the getState() and useState() methods on your Aggregate class.

    getState

    Override this method to define which data should be saved in the snapshot. It must return an array.

    useState

    Override this method to define how the state array should be applied back to the aggregate instance when it is being rehydrated.

    // Example of overriding getState to control snapshot data
    protected function getState(): array
    {
        return [
            'some_property' => $this->someProperty,
        ];
    }
    
    // Example of overriding useState to restore state
    protected function useState(array $state): void
    {
        $this->someProperty = $state['some_property'];
    }