PHP code example of projectsaturnstudios / pss-event-sourcing

1. Go to this page and download the library: Download projectsaturnstudios/pss-event-sourcing library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

projectsaturnstudios / pss-event-sourcing example snippets


// config/event-sourcing.php
// ...
    'event_serializer' => \ProjectSaturnStudios\EventSourcing\Serializers\EventSerializer::class,
// ...

// config/event-sourcing.php
'event_serializer' => \ProjectSaturnStudios\EventSourcing\Serializers\EventSerializer::class,

use ProjectSaturnStudios\EventSourcing\Middleware\RetryPersistMiddleware;

// Example: Manually adding to a command bus
$bus->middleware(new RetryPersistMiddleware(maximumTries: 5));

// 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.
}

// 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' => '

// Typically in a Controller, another Action, or a Service class

use App\Domain\Todo\Commands\CreateTodoTask; // Your EventCommand

// ...

// Instantiate your command with the ware 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.

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, []);

// 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;
    }
}

// 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.)
}

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
        // 

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';
        });
    }
}

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 ...
    }
}

    $eventsQueue = app_queue('events'); // Results in 'my_app_prod-events'
    $notificationsQueue = app_queue('notifications'); // Results in 'my_app_prod-notifications'
    

// 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
bash
php artisan vendor:publish --provider="Spatie\EventSourcing\EventSourcingServiceProvider" --tag="event-sourcing-config"