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
Requires
- php: >=8.2
- psr/container: ^2.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.52
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.5.15
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
Keep Commands and Events Immutable: Use
readonlyclasses to ensure messages cannot be modified after creation.Record Events in Entities: Use the
EventAwareTraitto record domain events within your entities, not in application services.One Handler Per Command: Each command should have exactly one handler.
Multiple Handlers Per Event: Events can have zero or more handlers (subscribers).
Type Safety: Leverage PHPStan generics to ensure type safety in your message buses.
Command Naming: Commands should be named in imperative form (e.g.,
CreateTicket,UpdateUser).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
- Yury Ksenevich - yury@spadar.com