friends-of-ddd/event-driven

Interfaces for event-driven architecture

Installs: 23

Dependents: 1

Suggesters: 0

Security: 0

Stars: 0

Forks: 0

pkg:composer/friends-of-ddd/event-driven

0.3.0 2025-11-11 21:25 UTC

This package is auto-updated.

Last update: 2025-11-11 20:27:03 UTC


README

An abstraction for event bus and command bus patterns following Domain-Driven Design principles.

This library provides a set of interfaces and utilities to implement event-driven architecture in PHP applications, supporting both Command Bus and Event Bus patterns with type-safe message handling.

Requirements

  • PHP: >= 8.2
  • Dependencies:
    • psr/container: ^2.0

Installation

Install the package via Composer:

composer require friends-of-ddd/event-driven

Features

  • 🎯 Command Bus Pattern: Type-safe command dispatching with generic support
  • 📢 Event Bus Pattern: Event-driven architecture support
  • 🧩 Event-Aware Entities: Built-in trait for domain entities to record and dispatch events
  • 📦 Message Collections: Type-safe collections for managing messages with filtering capabilities
  • 🔒 Type Safety: Full PHPStan level 8 compatibility with generics support
  • 🎨 PSR Compliant: Follows PSR standards and best practices

Core Concepts

Messages

All messages (commands and events) implement the base MessageInterface:

<?php

use FriendsOfDdd\EventDriven\Domain\MessageInterface;

interface MessageInterface
{
}

Commands

Commands represent intentions to perform an action in your application:

<?php

use FriendsOfDdd\EventDriven\Application\CommandInterface;

final readonly class CreateTicketCommand implements CommandInterface
{
    public function __construct(
        public string $title,
        public int $clientId,
        public ?int $topicId = null,
    ) {
    }
}

Events

Events represent something that has happened in your domain:

<?php

use FriendsOfDdd\EventDriven\Domain\EventInterface;

final readonly class TicketCreatedEvent implements EventInterface
{
    public function __construct(
        public int $ticketId,
        public string $title,
    ) {
    }
}

Usage Examples

1. Command Bus

Implement the CommandBusInterface to create a command bus. For example, wrap a symfony messenger command bus:

<?php

use FriendsOfDdd\EventDriven\Application\CommandInterface;
use FriendsOfDdd\EventDriven\Application\CommandBusInterface;
use Symfony\Component\Messenger\MessageBusInterface;

final readonly class CommandBus implements CommandBusInterface
{
    public function __construct(
        private MessageBusInterface $commandBus,
    ) {
    }

    public function dispatch(CommandInterface ...$commands): void
    {
        foreach ($commands as $command) {
            $this->commandBus->dispatch($command);
        }
    }
}

Using the Command Bus:

<?php

// Create a command
$command = new CreateTicketCommand(
    title: 'Fix login issue',
    clientId: 123,
    topicId: 5,
);

// Dispatch the command
$commandBus->dispatch($command);

// You can also dispatch multiple commands at once
$commandBus->dispatch(
    new CreateTicketCommand('First ticket', 123, 5),
    new CreateTicketCommand('Second ticket', 124, 6),
);

2. Event Bus

Implement the EventBusInterface to create an event bus. For example, wrap a symfony messenger command bus:

<?php

use FriendsOfDdd\EventDriven\Application\EventBusInterface;
use FriendsOfDdd\EventDriven\Domain\EventInterface;
use Psr\Container\ContainerInterface;
use Symfony\Component\Messenger\MessageBusInterface;

final class EventBus implements EventBusInterface
{
    public function __construct(
        private MessageBusInterface $eventBus,
    ) {
    }

    public function dispatch(EventInterface ...$events): void
    {
        foreach ($events as $event) {
            $this->eventBus->dispatch($event);
        }
    }
}

Using the Event Bus:

<?php

// Create an event
$event = new TicketCreatedEvent(1, 'Ticket title');

// Dispatch the event
$eventBus->dispatch($event);

// You can also dispatch multiple events at once
$eventBus->dispatch(
    new TicketCreatedEvent(2, 'Another ticket'),
    new TicketCreatedEvent(3, 'Third ticket'),
);

3. Event-Aware Entities

Use the EventAwareTrait to enable domain entities to record events:

<?php

use FriendsOfDdd\EventDriven\Domain\EventAwareTrait;

class TicketEntity
{
    use EventAwareTrait;

    private function __construct(
        public readonly int $id,
        private string $title,
        public readonly int $clientId,
        private ?int $topicId = null,
    ) {
    }

    public static function createNew(
        int $id,
        string $title,
        int $clientId,
        ?int $topicId = null,
    ): self {
        $instance = new self($id, $title, $clientId, $topicId);
        
        // Record domain event
        $instance->recordEvents(
            new TicketCreatedEvent($id, $title)
        );

        return $instance;
    }

    public function updateTitle(string $newTitle): void
    {
        $this->title = $newTitle;
        
        // Record multiple events
        $this->recordEvents(
            new TicketUpdatedEvent($this->id),
            new TitleChangedEvent($this->id, $newTitle),
        );
    }

    public function getTitle(): string
    {
        return $this->title;
    }
}

Retrieving and Dispatching Recorded Events:

<?php

// Create a new ticket
$ticket = TicketEntity::createNew(
    id: 1,
    title: 'Login issue',
    clientId: 123,
);

// Pop recorded events (this clears the internal event collection)
$events = $ticket->popRecordedEvents();

// Dispatch events to an event bus
foreach ($events as $event) {
    $eventBus->dispatch($event);
}

// Or dispatch all at once if your bus supports it
$eventBus->dispatch(...$events->toArray());

4. Complete Example: Ticket System

Here's a complete example showing all components working together:

<?php

namespace App\Ticketing;

use FriendsOfDdd\EventDriven\Application\CommandInterface;
use FriendsOfDdd\EventDriven\Application\CommandBusInterface;
use FriendsOfDdd\EventDriven\Application\EventBusInterface;
use FriendsOfDdd\EventDriven\Domain\EventInterface;
use FriendsOfDdd\EventDriven\Domain\EventAwareTrait;

// 1. Define Commands
final readonly class CreateTicketCommand implements CommandInterface
{
    public function __construct(
        public string $title,
        public int $clientId,
    ) {
    }
}

// 2. Define Events
final readonly class TicketCreatedEvent implements EventInterface
{
    public function __construct(
        public int $ticketId,
        public string $title,
    ) {
    }
}

// 3. Create Entity
class Ticket
{
    use EventAwareTrait;

    private function __construct(
        public readonly int $id,
        public readonly string $title,
        public readonly int $clientId,
    ) {
    }

    public static function create(int $id, string $title, int $clientId): self
    {
        $ticket = new self($id, $title, $clientId);
        $ticket->recordEvents(new TicketCreatedEvent($id, $title));
        return $ticket;
    }
}

// 4. Create Command Handler
final class CreateTicketCommandHandler
{
    public function __construct(
        private readonly TicketRepository $repository,
        private readonly CommandBusInterface $commandBus,
        private readonly EventBusInterface $eventBus,
    ) {
    }

    public function handle(CreateTicketCommand $command): void
    {
        $ticket = Ticket::create(
            id: $this->repository->nextId(),
            title: $command->title,
            clientId: $command->clientId,
        );

        $this->repository->save($ticket);

        // Dispatch domain events
        $this->eventBus->dispatch(...$ticket->popRecordedEvents()->toArray());
    }
}

// 5. Create Event Handler
final class NotifyClientWhenTicketCreated
{
    public function __construct(
        private readonly NotificationService $notificationService,
    ) {
    }

    public function handle(TicketCreatedEvent $event): void
    {
        $this->notificationService->notify(
            "Ticket #{$event->ticketId} has been created"
        );
    }
}

// 6. Usage
$command = new CreateTicketCommand('Fix login bug', 123);
$commandBus->dispatch($command);

Testing

The library includes a test implementation of a command bus that can be useful for testing:

<?php

use FriendsOfDdd\EventDriven\Tests\Kit\Messaging\TestCommandBus;

$commandBus = new TestCommandBus();

// Register handlers
$commandBus->addCommandHandler(
    CreateTicketCommand::class,
    function (CreateTicketCommand $command) {
        // Handle command in test
        $this->assertSame('Expected title', $command->title);
    }
);

// Dispatch command
$commandBus->dispatch(new CreateTicketCommand('Expected title', 123));

Best Practices

  1. Keep Commands and Events Immutable: Use readonly classes to ensure messages cannot be modified after creation.

  2. Record Events in Entities: Use the EventAwareTrait to record domain events within your entities, not in application services.

  3. One Handler Per Command: Each command should have exactly one handler.

  4. Multiple Handlers Per Event: Events can have zero or more handlers (subscribers).

  5. Type Safety: Leverage PHPStan generics to ensure type safety in your message buses.

  6. Command Naming: Commands should be named in imperative form (e.g., CreateTicket, UpdateUser).

  7. Event Naming: Events should be named in past tense (e.g., TicketCreated, UserUpdated).

License

This library is licensed under the MIT License. See the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Author