projectsaturnstudios / pss-event-sourcing
Spatie Event Sourcing using Spatie Laravel-Data Objects!
Requires
- php: ^8.2
- ext-pdo: *
- lorisleiva/laravel-actions: ^2.6
- ramsey/uuid: ^4.7
- spatie/laravel-data: ^4.11
- spatie/laravel-event-sourcing: ^7.3
README
Supercharge your Spatie Event Sourcing with the elegance of Spatie's Laravel-Data objects and the power of Loris Leiva's Laravel Actions. This package is all about making event sourcing in Laravel a smoother, more typed, and downright enjoyable ride.
📜 Table of Contents
- 🤔 Why This Package?
- ✨ Features List
- Installation
- Configuration
- How to Rock It (Usage Guide)
- Testing Your Masterpiece
- Advanced Scenarios / Cookbook
- Contributing
- Support & Contact
- License
- Credits & Thanks
- README Change Log
🤔 Why This Package?
Event sourcing is a powerful pattern, and Spatie's laravel-event-sourcing
package provides a rock-solid foundation for it in Laravel. So, why add another layer with laravel-es-utilities
? Glad you asked, partner. This package is all about taking that solid foundation and making your developer experience (DX) even smoother, more robust, and, dare we say, more enjoyable.
Here's the lowdown on what this package brings to your shindig:
-
🤠 Enhanced Type Safety & Clarity with
DataEvent
s:- Problem: Traditional event payloads can sometimes be loose arrays or objects, leading to uncertainty about their structure.
- Solution: We integrate
spatie/laravel-data
directly into your event sourcing workflow. By defining your events as strongly-typedDataEvent
objects (which are essentiallySpatie\LaravelData\Data
objects tailored for event sourcing), you get auto-completion, compile-time(ish) checks via static analysis, and runtime validation. This means fewer bugs and clearer contracts for what your events represent.
-
🎬 Structured & Testable Commands with
EventCommand
s:- Problem: Command handling logic can sometimes become scattered or less defined.
- Solution: Leveraging
lorisleiva/laravel-actions
, we promote a pattern ofEventCommand
classes. These are dedicated action classes that encapsulate the logic for validating a command, executing business rules, and ultimately producing aDataEvent
. This makes your command logic more organized, easier to test in isolation, and reusable.
-
⚙️ Streamlined Workflow:
- Problem: Wiring up custom serializers or ensuring robust event persistence can require boilerplate.
- Solution: This package comes with:
- A pre-configured
EventSerializer
that understands how to handleDataEvent
objects out-of-the-box. - A
RetryPersistMiddleware
(applied by default via theevent_command()
helper) that automatically retries event persistence on common database issues, adding resilience to your system. - The convenient
event_command()
helper function to dispatch yourEventCommand
s with sensible defaults.
- A pre-configured
-
🚀 Improved Developer Experience (DX):
- Overall Benefit: By combining these elements, the goal is to reduce boilerplate, increase clarity, improve testability, and make the overall process of building event-sourced applications in Laravel more intuitive and less error-prone. You get to focus more on your business logic and less on the plumbing.
In short, if you love spatie/laravel-event-sourcing
but wished for tighter integration with typed data objects and a more structured approach to commands, laravel-es-utilities
is here to be your new best friend.
✨ Features List
This package is packed with goodies to make your event sourcing journey in Laravel smoother. Here are the highlights:
-
Typed
DataEvent
Objects:- Leverages
spatie/laravel-data
to allow you to define your events as strongly-typed Data objects. - Events should extend
ProjectSaturnStudios\EventSourcing\Events\DataEvent
. - Benefits: Clear event contracts, auto-completion, validation, and easier refactoring.
- Leverages
-
Structured
EventCommand
Actions:- Promotes the use of
lorisleiva/laravel-actions
for handling commands. - Commands can extend the base
ProjectSaturnStudios\EventSourcing\EventCommands\EventCommand
. - Benefits: Encapsulated command logic, improved testability, and reusability of commands.
- Promotes the use of
-
Convenient
event_command()
Helper:- A global helper function
event_command(YourEventCommand $command, array $middleware = [...])
to easily dispatch yourEventCommand
instances. - Automatically applies
RetryPersistMiddleware
by default.
- A global helper function
-
Resilient
RetryPersistMiddleware
:- Spatie Command Bus middleware that automatically retries event persistence if common database issues (like
PDOException
orCouldNotPersistAggregate
) occur. - Enhances the reliability of your event stream.
- Spatie Command Bus middleware that automatically retries event persistence if common database issues (like
-
Tailored
EventSerializer
:- A custom event serializer (
ProjectSaturnStudios\EventSourcing\Serializers\EventSerializer
) that understands how to serialize and deserialize yourDataEvent
objects usingspatie/laravel-data
's capabilities. - Automatically configured by the package's service provider.
- A custom event serializer (
-
PSSEventSourcingServiceProvider
:- The package's service provider automatically merges necessary configurations, primarily setting up the custom
EventSerializer
.
- The package's service provider automatically merges necessary configurations, primarily setting up the custom
-
Flexible
AlternativeAggregateRoot
:- An optional base class (
ProjectSaturnStudios\EventSourcing\Aggregates\AlternativeAggregateRoot
) for your aggregates if you need to specify custom event or snapshot repositories.
- An optional base class (
-
Helpful
app_queue()
Helper:- A utility function
app_queue('your-queue-name')
to easily prefix queue names based on theQS_PREFIX
environment variable, useful for namespacing.
- A utility function
-
Centralized
DispatchEventCommand
Action:- An underlying action (
ProjectSaturnStudios\EventSourcing\Actions\DispatchEventCommand
) used by theevent_command()
helper to integrate with Spatie's Command Bus and apply middleware.
- An underlying action (
📦 Installation
You can install the package via Composer:
composer require projectsaturnstudios/laravel-es-utilities
The package leverages Laravel's package auto-discovery, so you typically won't need to manually register the service provider (ProjectSaturnStudios\EventSourcing\Providers\PSSEventSourcingServiceProvider
).
This service provider will automatically merge the necessary configuration to use the custom ProjectSaturnStudios\EventSourcing\Serializers\EventSerializer
for handling DataEvent
objects. This ensures your typed event data is correctly serialized and deserialized using spatie/laravel-data
capabilities.
Optional: Publishing Configuration
If you wish to customize the event sourcing configuration further (beyond what this package provides by default), you can publish Spatie's main event sourcing configuration file. Note that this package already sets the recommended event_serializer
. If you publish and modify Spatie's config, ensure you retain or re-configure the event_serializer
to point to ProjectSaturnStudios\EventSourcing\Serializers\EventSerializer::class
if you want to continue using this package's DataEvent serialization.
To publish Spatie's configuration (if you haven't already for spatie/laravel-event-sourcing
):
php artisan vendor:publish --provider="Spatie\EventSourcing\EventSourcingServiceProvider" --tag="event-sourcing-config"
This will create a config/event-sourcing.php
file in your application's config directory.
Again, for this package (projectsaturnstudios/laravel-es-utilities
) to work as intended with DataEvent
objects, the event_serializer
in that published config should be:
// config/event-sourcing.php // ... 'event_serializer' => \ProjectSaturnStudios\EventSourcing\Serializers\EventSerializer::class, // ...
Our package attempts to set this for you by default through its own service provider's config merging. Publishing is only needed if you require deeper customization of other Spatie event sourcing settings.
⚙️ Configuration
This package is designed to work with minimal configuration out-of-the-box.
Event Serializer
The most crucial piece of configuration is the event_serializer
. Our ProjectSaturnStudios\EventSourcing\Providers\PSSEventSourcingServiceProvider
automatically merges its configuration to set the event_serializer
in your config/event-sourcing.php
to use ProjectSaturnStudios\EventSourcing\Serializers\EventSerializer::class
.
This ensures that your DataEvent
objects are correctly handled. No manual configuration is typically needed for this.
If you publish the spatie/laravel-event-sourcing
configuration file (php artisan vendor:publish --provider="Spatie\EventSourcing\EventSourcingServiceProvider" --tag="event-sourcing-config"
), you'll see this setting:
// config/event-sourcing.php 'event_serializer' => \ProjectSaturnStudios\EventSourcing\Serializers\EventSerializer::class,
If you override this, ensure you understand the implications for DataEvent
serialization.
Queue Prefixing (app_queue()
helper)
The app_queue(string $key): string
helper function allows you to prefix your queue names. It looks for an environment variable:
QS_PREFIX
: If set, this prefix (e.g.,my-app-
) will be prepended to the queue key you provide. Example:app_queue('events')
withQS_PREFIX=prod
would result inprod-events
.
This is useful for namespacing queues in different environments or applications sharing a Redis instance.
RetryPersistMiddleware
The ProjectSaturnStudios\EventSourcing\Middleware\RetryPersistMiddleware
used by the event_command()
helper defaults to 3 retry attempts. If you were to use this middleware manually with Spatie's Command Bus, you could configure the number of retries via its constructor:
use ProjectSaturnStudios\EventSourcing\Middleware\RetryPersistMiddleware; // Example: Manually adding to a command bus $bus->middleware(new RetryPersistMiddleware(maximumTries: 5));
However, when using the event_command()
helper, it defaults to 3 retries, which is generally a sensible default.
Other Spatie Event Sourcing Configuration
For all other event sourcing configurations (e.g., projectors, reactors, stored event repository, snapshot repository), please refer to the official spatie/laravel-event-sourcing
package documentation and its config/event-sourcing.php
file. This package primarily focuses on enhancing the event and command handling aspects.
🚀 How to Rock It (Usage Guide)
Alright, let's get down to brass tacks. Here's how you can use this package to make your event sourcing life a little more zen.
Defining Your DataEvent
First up, you'll want to define your domain events as DataEvent
objects. These are essentially Spatie\LaravelData\Data
objects that also extend ProjectSaturnStudios\EventSourcing\Events\DataEvent
. This base class provides the necessary integration with Spatie's event sourcing system and Laravel Data's features.
Example: Let's say we're building a simple "Todo" application and want an event for when a task is created.
// app/Domain/Todo/Events/TodoTaskCreated.php namespace App\Domain\Todo\Events; use ProjectSaturnStudios\EventSourcing\Events\DataEvent; use Spatie\LaravelData\Attributes\MapName; use Spatie\LaravelData\Mappers\SnakeCaseMapper; // Optional: for snake_case mapping in DB use Ramsey\Uuid\UuidInterface; // Or string if you prefer #[MapName(SnakeCaseMapper::class)] // Optional, if you want stored event payload keys to be snake_case class TodoTaskCreated extends DataEvent { public function __construct( public readonly UuidInterface $taskUuid, // Or string for UUID public readonly string $description, public readonly string $status, // e.g., "pending", "completed" public readonly \DateTimeImmutable $createdAt ) {} // You typically don't need to define an eventType() method here. // Spatie's event sourcing often uses the fully qualified class name by default, // or you can configure aliases in config/event-sourcing.php if needed. }
Key Benefits Here:
- Strong Typing:
taskUuid
,description
,status
, andcreatedAt
are all typed. - Readonly Properties: Good practice for immutable event data.
- Laravel Data Features: You can use all of
spatie/laravel-data
's features like casting, validation attributes (though validation is often done in commands), custom creators, etc., if needed. - Serialization: The configured
EventSerializer
will automatically uselaravel-data
'stoJson()
andfrom()
methods.
Crafting Your EventCommand
Next, you'll create EventCommand
classes. These are responsible for taking input, performing any necessary validation or business logic, and then creating the appropriate DataEvent
. We recommend using lorisleiva/laravel-actions
for these commands, and they can extend the base ProjectSaturnStudios\EventSourcing\EventCommands\EventCommand
(though this base class is simple and mainly for type-hinting or namespacing).
Example: A command to create our TodoTask
.
// app/Domain/Todo/Commands/CreateTodoTask.php namespace App\Domain\Todo\Commands; use ProjectSaturnStudios\EventSourcing\EventCommands\EventCommand; // Optional base class use Lorisleiva\LaravelActions\Concerns\AsAction; use App\Domain\Todo\Events\TodoTaskCreated; // The DataEvent this command will produce use App\Domain\Todo\Models\TodoTaskAggregate; // Assuming an aggregate for TodoTask use Ramsey\Uuid\Uuid; class CreateTodoTask extends EventCommand // Or just use AsAction without extending { use AsAction; // Properties to hold the command data, matching the handle method parameters public string $description; public function __construct(string $description) { $this->description = $description; } // The handle method contains the core logic of your command public function handle(): void // Command handlers are typically void { $taskUuid = Uuid::uuid4(); $initialStatus = 'pending'; // Or from config, etc. $currentTime = new \DateTimeImmutable(); // Create the DataEvent $event = new TodoTaskCreated( taskUuid: $taskUuid, description: $this->description, status: $initialStatus, createdAt: $currentTime ); // Here, you'd typically interact with an AggregateRoot // For a creation command, you might instantiate a new aggregate // or use a static factory method on the aggregate. /** @var TodoTaskAggregate $todoAggregate */ // Option 1: Static constructor if your aggregate supports it // $todoAggregate = TodoTaskAggregate::createWithEvent($taskUuid, $event); // Option 2: Retrieve (if applicable, not for creation usually) then record // This pattern is more for updates, but showing the recordThat flow: // $todoAggregate = TodoTaskAggregate::retrieve($taskUuid); // Won't exist for creation // $todoAggregate->recordThat($event); // For creation, a common pattern with Spatie's package is to have // the aggregate handle its own instantiation via a command-like method // or by applying a creation event to a fresh instance. // Let's assume a simplified aggregate method for this example: // This is a conceptual example; your aggregate's API may differ. TodoTaskAggregate::new($taskUuid) // Gets a new aggregate instance ->recordAndApply($event) // Custom method to record and apply (or just recordThat) ->persist(); // Persist the aggregate and its events } // Optional: Add Laravel Action features like validation public function rules(): array { return [ 'description' => 'required|string|min:3|max:255', ]; } // You can also define authorization, middleware, etc., as per laravel-actions documentation. }
Key Points for EventCommand
s:
- Single Responsibility: Each command handles one specific action.
- Data In, Event (indirectly) Out: Takes input data, produces a
DataEvent
which is then recorded on an aggregate. - Validation: Use
laravel-actions
' validation features to ensure data integrity before processing. - Interaction with Aggregates: The command handler is where you'll typically retrieve an aggregate, call its methods (which in turn record events), and persist the aggregate.
Dispatching with event_command()
Once you have your EventCommand
defined, you'll want to dispatch it. This package provides a convenient global helper function event_command()
for this purpose.
This helper function uses the ProjectSaturnStudios\EventSourcing\Actions\DispatchEventCommand
action internally, which in turn uses Spatie's Command Bus. By default, if you don't specify any middleware, event_command()
applies the ProjectSaturnStudios\EventSourcing\Middleware\RetryPersistMiddleware
.
Example: Dispatching our CreateTodoTask
command.
// Typically in a Controller, another Action, or a Service class use App\Domain\Todo\Commands\CreateTodoTask; // Your EventCommand // ... // Instantiate your command with the required data $description = 'Finish writing this awesome README'; $command = new CreateTodoTask(description: $description); // Dispatch it using the helper! This will use RetryPersistMiddleware by default. event_command($command); // The `handle()` method of your CreateTodoTask action will be executed. // If it successfully creates and persists the event via an aggregate, you're golden! // If a PDOException or CouldNotPersistAggregate occurs during the process, // the RetryPersistMiddleware will automatically attempt to retry.
If you need to use custom middleware, or want to change the retry behavior:
use App\Http\Middleware\SomeCustomMiddleware; // Example custom middleware use ProjectSaturnStudios\EventSourcing\Middleware\RetryPersistMiddleware; // Dispatch with only your custom middleware (no retry by default in this case): event_command($command, [new SomeCustomMiddleware()]); // Dispatch with your custom middleware AND the retry middleware: event_command($command, [ new SomeCustomMiddleware(), new RetryPersistMiddleware(maximumTries: 5) // Optionally configure retries ]); // Dispatch with NO middleware at all (if you have a specific reason): event_command($command, []);
Working with Aggregates
Your EventCommand
handlers will typically interact with Aggregate Roots (from spatie/laravel-event-sourcing
). The aggregate is responsible for upholding business rules and recording events that result from command processing.
Example Aggregate (TodoTaskAggregate
):
Let's sketch out what our TodoTaskAggregate
might look like. It would extend Spatie\EventSourcing\AggregateRoots\AggregateRoot
(or our ProjectSaturnStudios\EventSourcing\Aggregates\AlternativeAggregateRoot
if you need its specific features).
// app/Domain/Todo/Models/TodoTaskAggregate.php namespace App\Domain\Todo\Models; // Adjust namespace as needed use Spatie\EventSourcing\AggregateRoots\AggregateRoot; use App\Domain\Todo\Events\TodoTaskCreated; // Our DataEvent use App\Domain\Todo\Events\TodoTaskDescriptionChanged; // Another example event use Ramsey\Uuid\UuidInterface; class TodoTaskAggregate extends AggregateRoot { protected string $description = ''; protected string $status = ''; // Static "named constructor" for creation, often called by a command public static function createTask(UuidInterface $uuid, string $description, string $initialStatus): self { $aggregateRoot = static::retrieve($uuid->toString()); // Get a new or existing instance mapped to UUID // Don't record if already created (idempotency for creation can be tricky, // often command validation should check if an entity with this ID/params already exists) // For simplicity here, we assume the command ensures this is a new task. $aggregateRoot->recordThat(new TodoTaskCreated( taskUuid: $uuid, description: $description, status: $initialStatus, createdAt: new \DateTimeImmutable() )); return $aggregateRoot; // Return for persistence in the command } // Public method to change the description public function changeDescription(string $newDescription): self { if ($this->description === $newDescription) { // No change needed, don't record an event return $this; } if (empty(trim($newDescription))) { // Or throw a domain exception throw new \InvalidArgumentException('Description cannot be empty.'); } $this->recordThat(new TodoTaskDescriptionChanged( taskUuid: Uuid::fromString($this->uuid()), // Assumes $this->uuid() returns the string UUID newDescription: $newDescription, changedAt: new \DateTimeImmutable() )); return $this; } // Apply methods for each event type protected function applyTodoTaskCreated(TodoTaskCreated $event): void { $this->description = $event->description; $this->status = $event->status; // Other properties from the event can be set here } protected function applyTodoTaskDescriptionChanged(TodoTaskDescriptionChanged $event): void { $this->description = $event->newDescription; } }
Key Interactions:
- Recording Events: Inside your aggregate methods (like
createTask
orchangeDescription
), you call$this->recordThat(new YourDataEvent(...))
to stage an event. - Applying Events: For each event type, you create a corresponding
apply<EventName>
method (e.g.,applyTodoTaskCreated
). This method is responsible for changing the state of the aggregate based on the event data. This is how the aggregate rebuilds its state when loaded from history. - Persisting: After calling methods on your aggregate in your
EventCommand
, you must call$aggregate->persist()
to save all recorded events to the database.
Revised CreateTodoTask
Command (showing aggregate interaction more clearly):
// app/Domain/Todo/Commands/CreateTodoTask.php // ... (namespace, uses etc.) class CreateTodoTask extends EventCommand { use AsAction; public string $description; public function __construct(string $description) { $this->description = $description; } public function handle(): void { $taskUuid = Uuid::uuid4(); $initialStatus = 'pending'; // Call the static method on the aggregate to handle event recording $aggregate = TodoTaskAggregate::createTask( uuid: $taskUuid, description: $this->description, initialStatus: $initialStatus ); // Persist the aggregate (this saves the recorded TodoTaskCreated event) $aggregate->persist(); } // ... (rules etc.) }
Implicit Benefits: RetryPersistMiddleware
& EventSerializer
It's worth reiterating two key components that work quietly in the background to make your life easier when using the patterns described above:
-
EventSerializer
:- As configured by this package, the
ProjectSaturnStudios\EventSourcing\Serializers\EventSerializer
automatically handles the serialization of yourDataEvent
objects to JSON for storage and deserialization back into their strongly-typedSpatie\LaravelData\Data
forms when events are retrieved. - You generally don't need to think about this; it just works, ensuring your typed event data is preserved correctly.
- As configured by this package, the
-
RetryPersistMiddleware
:- When you dispatch commands using the
event_command()
helper, theProjectSaturnStudios\EventSourcing\Middleware\RetryPersistMiddleware
is active by default. - If your command handler attempts to persist an aggregate (
$aggregate->persist()
) and a transient database error occurs (like aPDOException
or Spatie'sCouldNotPersistAggregate
), this middleware will automatically retry the entire command execution a few times. - This adds a significant layer of resilience against temporary network glitches or database deadlocks without you needing to write custom retry logic in every command.
- When you dispatch commands using the
These features are designed to provide a more robust and developer-friendly experience with minimal effort on your part.
✅ Testing Your Masterpiece
Testing is crucial for event-sourced applications. Here's how you can approach testing different parts of your system when using laravel-es-utilities
.
Testing EventCommand
Actions
Since your EventCommand
classes are built using lorisleiva/laravel-actions
, you can test them like any other Laravel Action. You can unit test them by directly instantiating and calling the handle
method, or test them as part of a feature test.
Key things to assert:
- Validation Rules: Ensure your command's
rules()
method correctly validates input. - Event Emission (via Aggregates): The primary goal is often to ensure the correct
DataEvent
(s) are recorded on an aggregate. - State Changes on Aggregates: Verify that the aggregate's state changes as expected after applying the events triggered by the command.
Spatie's laravel-event-sourcing
package provides excellent testing tools for aggregates:
use App\Domain\Todo\Commands\CreateTodoTask; use App\Domain\Todo\Events\TodoTaskCreated; use App\Domain\Todo\Models\TodoTaskAggregate; use Illuminate\Foundation\Testing\RefreshDatabase; // Or your preferred DB setup use Tests\TestCase; // Your base TestCase class CreateTodoTaskTest extends TestCase { use RefreshDatabase; // If your commands interact with the DB beyond event store /** @test */ public function it_creates_a_todo_task_and_records_event() { $description = 'Write comprehensive tests'; // $command = new CreateTodoTask(description: $description); // Execute the command // event_command($command); // Given our current EventCommand design (handle is void and no UUID is returned directly from event_command): // Testing the full flow initiated by event_command() and then asserting specific aggregate events // requires a strategy to retrieve or predict the aggregate UUID. // For a more direct test of the command's interaction with the aggregate: $taskUuid = \Ramsey\Uuid\Uuid::uuid4(); // Simulate what the command's handle method does: $aggregate = TodoTaskAggregate::createTask($taskUuid, $description, 'pending'); $aggregate->persist(); // Manually persist for this test setup $retrievedAggregate = TodoTaskAggregate::retrieve($taskUuid->toString()); $retrievedAggregate->assertRecorded([ TodoTaskCreated::class, ]); $retrievedAggregate->assertEventRecorded(TodoTaskCreated::class, function(TodoTaskCreated $event) use ($description) { return $event->description === $description; }); } }
Testing Aggregates
Spatie's laravel-event-sourcing
package provides powerful testing methods directly on your aggregate roots.
use App\Domain\Todo\Models\TodoTaskAggregate; use App\Domain\Todo\Events\TodoTaskCreated; use App\Domain\Todo\Events\TodoTaskDescriptionChanged; use Ramsey\Uuid\Uuid; use Tests\TestCase; class TodoTaskAggregateTest extends TestCase { /** @test */ public function it_can_create_a_task() { $uuid = Uuid::uuid4(); $aggregate = TodoTaskAggregate::createTask($uuid, 'My first task', 'pending'); // $aggregate->persist(); // Persist if you want to check DB, not needed for assertRecorded on instance $aggregate->assertRecorded([ TodoTaskCreated::class, ]); } /** @test */ public function it_can_change_its_description() { $uuid = Uuid::uuid4(); $aggregate = TodoTaskAggregate::createTask($uuid, 'Old description', 'pending'); // Clear recorded events from creation if you only want to test the change // $aggregate->flushRecordedEvents(); $aggregate->changeDescription('New shiny description'); $aggregate->assertEventRecorded(TodoTaskDescriptionChanged::class, function (TodoTaskDescriptionChanged $event) { return $event->newDescription === 'New shiny description'; }); } }
Key aggregate testing methods:
$aggregateRoot->assertRecorded($expectedEvents)
$aggregateRoot->assertNotRecorded($unexpectedEvents)
$aggregateRoot->assertEventRecorded($eventClass, Closure $callback = null)
$aggregateRoot->assertNothingRecorded()
$aggregateRoot->flushRecordedEvents()
Testing Projectors and Reactors
Refer to the spatie/laravel-event-sourcing
documentation for detailed guidance on testing projectors and reactors. They also have dedicated testing tools.
This package focuses on the command and event creation side, so ensure your DataEvent
objects and EventCommand
actions integrate well with your aggregates, and then leverage Spatie's tools for the rest.
🔮 Advanced Scenarios / Cookbook
Here are a couple of scenarios or tools within the package that go beyond the basic setup.
Using AlternativeAggregateRoot
If you have specific needs for how your aggregate roots interact with event or snapshot repositories (for example, using different database connections or custom repository implementations for certain aggregates), this package provides an AlternativeAggregateRoot
.
You can extend ProjectSaturnStudios\EventSourcing\Aggregates\AlternativeAggregateRoot
instead of the default Spatie\EventSourcing\AggregateRoots\AggregateRoot
. Then, within your aggregate, you can specify the classes for your custom repositories:
namespace App\Domain\Special\Aggregates; use ProjectSaturnStudios\EventSourcing\Aggregates\AlternativeAggregateRoot; use App\Domain\Special\Repositories\MyCustomEventRepository; // Your custom repo class use App\Domain\Special\Repositories\MyCustomSnapshotRepository; // Your custom repo class class MySpecialAggregate extends AlternativeAggregateRoot { // Point to your custom repository classes protected string $event_repo = MyCustomEventRepository::class; protected string $snapshot_repo = MyCustomSnapshotRepository::class; // ... rest of your aggregate logic ... public function doSomethingSpecial(): self { $this->recordThat(new SomethingSpecialHappened(/* ...data... */)); return $this; } protected function applySomethingSpecialHappened(SomethingSpecialHappened $event): void { // ... apply state ... } }
Your custom repository classes (MyCustomEventRepository
, MyCustomSnapshotRepository
) must implement the respective interfaces expected by Spatie's event sourcing package (e.g., Spatie\EventSourcing\StoredEvents\Repositories\StoredEventRepository
and Spatie\EventSourcing\Snapshots\SnapshotRepository
).
This gives you fine-grained control over persistence mechanisms for specific aggregates without altering the global defaults.
Using the app_queue()
Helper
The app_queue(string $key): string
helper function is a simple utility for prefixing your queue names based on an environment variable. This is particularly useful if you're running multiple applications or environments that might share a queueing backend (like Redis) and you want to avoid queue name collisions.
How it works:
- Define an environment variable, for example, in your
.env
file:QS_PREFIX=my_app_prod
- When you use the helper:
$eventsQueue = app_queue('events'); // Results in 'my_app_prod-events' $notificationsQueue = app_queue('notifications'); // Results in 'my_app_prod-notifications'
- If
QS_PREFIX
is not set or is empty,app_queue('events')
will simply return'events'
.
You can use this helper anywhere you define queue names, such as in your event listeners, jobs, or in the Spatie event sourcing configuration (config/event-sourcing.php
for the main event queue).
Example in config/event-sourcing.php
:
// config/event-sourcing.php /* * A queue is used to guarantee that all events get passed to the projectors in * the right order. Here you can set of the name of the queue. */ 'queue' => env('EVENT_PROJECTOR_QUEUE_NAME', app_queue('event-sourcing')), // Using the helper
This helps keep your queue management clean and organized across different environments.
🙌 Contributing
We welcome contributions from the community! If you're interested in contributing to this package, please follow these steps:
- Fork the repository on GitHub.
- Create a new branch for your feature or bug fix.
- Make your changes in the new branch.
- Write tests for your changes.
- Submit a pull request.
🙌 Support & Contact
If you encounter any issues or have questions about this package, please feel free to open an issue on GitHub or contact us directly.
📄 License
This package is open-source and released under the MIT License. See the LICENSE file for more information.
📋 Credits & Thanks
This package was created by Project Saturn Studios. We're grateful for the contributions of the community and the support of our users.
📜 README Change Log
See the CHANGELOG file for a detailed history of changes to this README.