Skip to content

Example

Example

This example demonstrates all key features of the library: a DCB use case with command handler, domain events, and a saga coordinating a workflow.

Scenario: A student subscribes to a course. When subscription succeeds, a saga automatically sends a welcome email.

Domain Events

use Gember\EventSourcing\UseCase\Attribute\DomainEvent;
use Gember\EventSourcing\UseCase\Attribute\DomainTag;
use Gember\EventSourcing\Saga\Attribute\SagaId;

#[DomainEvent(name: 'course.created')]
final readonly class CourseCreatedEvent
{
    public function __construct(
        #[DomainTag]
        public string $courseId,
        public string $name,
    ) {}
}

#[DomainEvent(name: 'student.registered')]
final readonly class StudentRegisteredEvent
{
    public function __construct(
        #[DomainTag]
        public string $studentId,
        public string $email,
    ) {}
}

#[DomainEvent(name: 'student.subscribed')]
final readonly class StudentSubscribedEvent
{
    public function __construct(
        #[DomainTag]
        #[SagaId]  // Links to SubscriptionWelcomeSaga
        public string $courseId,
        #[DomainTag]
        #[SagaId]
        public string $studentId,
    ) {}
}

Use Case with Command Handler

use Gember\EventSourcing\Common\CreationPolicy;
use Gember\EventSourcing\UseCase\Attribute\DomainCommandHandler;
use Gember\EventSourcing\UseCase\Attribute\DomainEventSubscriber;
use Gember\EventSourcing\UseCase\Attribute\DomainTag;
use Gember\EventSourcing\UseCase\EventSourcedUseCase;
use Gember\EventSourcing\UseCase\EventSourcedUseCaseBehaviorTrait;

final class SubscribeStudentToCourse implements EventSourcedUseCase
{
    use EventSourcedUseCaseBehaviorTrait;

    #[DomainTag]
    private CourseId $courseId;

    #[DomainTag]
    private StudentId $studentId;

    private bool $isSubscribed = false;

    /**
     * Subscribes a student to a course (DCB pattern with multiple domain tags).
     * Uses __invoke to emphasize this is a single-purpose use case.
     */
    #[DomainCommandHandler(policy: CreationPolicy::IfMissing)]
    public function __invoke(SubscribeStudentCommand $command): void
    {
        // 1. Check idempotency
        if ($this->isSubscribed) {
            return;
        }

        // 2. Protect invariants (simplified for example)
        // In real scenarios: check capacity, prerequisites, etc.

        // 3. Apply domain event
        $this->apply(new StudentSubscribedEvent(
            $command->courseId,
            $command->studentId,
        ));
    }

    #[DomainEventSubscriber]
    private function onCourseCreated(CourseCreatedEvent $event): void
    {
        $this->courseId = new CourseId($event->courseId);
    }

    #[DomainEventSubscriber]
    private function onStudentRegistered(StudentRegisteredEvent $event): void
    {
        $this->studentId = new StudentId($event->studentId);
    }

    #[DomainEventSubscriber]
    private function onStudentSubscribed(StudentSubscribedEvent $event): void
    {
        $this->isSubscribed = true;
    }
}

Saga

use Gember\EventSourcing\Common\CreationPolicy;
use Gember\EventSourcing\Saga\Attribute\Saga;
use Gember\EventSourcing\Saga\Attribute\SagaEventSubscriber;
use Gember\EventSourcing\Saga\Attribute\SagaId;
use Gember\EventSourcing\Saga\CommandRecorder;

#[Saga(name: 'subscription.welcome')]
final class SubscriptionWelcomeSaga
{
    #[SagaId]
    public ?string $courseId = null;

    #[SagaId]
    public ?string $studentId = null;

    private bool $welcomeEmailSent = false;

    /**
     * When a student subscribes, automatically send a welcome email.
     */
    #[SagaEventSubscriber(policy: CreationPolicy::IfMissing)]
    public function onStudentSubscribed(StudentSubscribedEvent $event, CommandRecorder $commandRecorder): void
    {
        $this->courseId = $event->courseId;
        $this->studentId = $event->studentId;

        // Record command to send welcome email (dispatched after saga is persisted)
        $commandRecorder->record(new SendWelcomeEmailCommand(
            $event->studentId,
            $event->courseId,
        ));

        $this->welcomeEmailSent = true;
    }
}

For more extended examples and complete implementations, check out the demo application gember/example-event-sourcing-dcb.