Skip to content

Commit c00b6d8

Browse files
committed
Documentation
1 parent 4d3660e commit c00b6d8

File tree

8 files changed

+1582
-365
lines changed

8 files changed

+1582
-365
lines changed

docs/core-concepts/event-bus.md

Lines changed: 80 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,14 @@
11
# Core Concepts: Event Bus
22

3-
While the Event Store persists events, the Event Bus allows other parts of the application to react to these events *after* they have been successfully stored. This is crucial for decoupling components and enabling patterns like building read models (CQRS), triggering notifications, or starting long-running processes (Sagas).
3+
An Event Bus is a central component responsible for dispatching [Domain Events](./domain-events.md) to interested [Event Listeners](./listeners.md).
44

5-
## What is an Event Bus?
5+
## Purpose
66

7-
An Event Bus acts as a central dispatch mechanism for events. Components called [Event Listeners](./listeners.md) register their interest in specific types of events with the bus. When an event is published to the bus, the bus ensures it is delivered to all interested listeners.
7+
The primary goal of an Event Bus is to **decouple event producers from event consumers (listeners)**. When an action occurs in your domain (e.g., a user registers, an order is placed), the code responsible for that action (e.g., a Command Handler) can publish the resulting event to the Event Bus without needing to know which specific components need to react to that event.
88

9-
Key characteristics:
9+
The Event Bus then routes the event to all registered listeners that are interested in that specific event type.
1010

11-
* **Decoupling:** The publisher of an event doesn't need to know anything about the listeners, and listeners don't know about the publisher.
12-
* **Asynchronous Potential:** While implementations can be synchronous, event buses are often used for asynchronous processing, where listeners handle events in the background.
13-
* **Multiple Listeners:** A single event can be handled by zero, one, or many listeners.
14-
15-
## The `EventBus` Interface
16-
17-
Streak defines a simple interface for the event bus contract in `Streak\Domain\EventBus`:
11+
## Key Interface (`Streak\Domain\EventBus`)
1812

1913
```php
2014
<?php
@@ -24,131 +18,124 @@ namespace Streak\Domain;
2418
interface EventBus
2519
{
2620
/**
27-
* Publishes an event (typically wrapped in an Envelope) to interested listeners.
28-
*
29-
* @param Event\Envelope $event
21+
* Registers a listener with the bus.
3022
*/
31-
public function publish(Event\Envelope $event): void;
23+
public function add(Event\Listener $listener): void;
3224

3325
/**
34-
* Subscribes a listener to specific events.
35-
*
36-
* Note: The exact mechanism for determining which events a listener handles
37-
* often depends on the implementation (e.g., type hinting, attributes,
38-
* explicit registration methods based on listensTo()).
39-
*
40-
* @param Event\Listener $listener
26+
* Unregisters a listener from the bus.
4127
*/
42-
public function subscribe(Event\Listener $listener): void;
28+
public function remove(Event\Listener $listener): void;
29+
30+
/**
31+
* Publishes one or more event envelopes to interested listeners.
32+
*/
33+
public function publish(Event\Envelope ...$events);
4334
}
4435
```
4536

46-
## Event Bus vs. Subscriptions
47-
48-
It's important to distinguish the role of the `EventBus` from persistent [Subscriptions](./subscriptions.md):
37+
* **`add(Listener $listener)`:** Registers a listener instance with the bus.
38+
* **`remove(Listener $listener)`:** Removes a previously registered listener.
39+
* **`publish(Envelope ...$events)`:** Takes one or more `Event\Envelope` objects and dispatches them to the relevant registered listeners.
4940

50-
* **Event Bus:** Primarily for *pushing* notifications about events that just happened. Good for immediate reactions, simple side effects, or triggering potentially transient listeners. Listeners rely on the bus for delivery.
51-
* **Subscriptions:** Primarily for *pulling* events from the `EventStore` in a reliable, stateful way. Ideal for critical tasks like building read models or running process managers that must process events exactly once and survive restarts. They manage their own progress independently of the bus.
41+
## Registering Listeners
5242

53-
Often, the `PublishingEventStore` publishes events to the `EventBus` *and* these events are also processed independently later by `Subscriptions` pulling from the store.
43+
Listeners must be registered with the Event Bus instance before they can receive events.
5444

55-
## Event Listeners
45+
```php // Example Registration (Conceptual)
46+
<?php
5647

57-
Event Listeners are components that react to specific domain events. They typically implement the `Streak\Domain\Event\Listener` interface.
48+
use Streak\Domain\EventBus;
49+
use Streak\Domain\Event\Listener;
5850

59-
```php
60-
<?php
51+
// Assume $bus is an EventBus instance and $listener is a Listener instance
6152

62-
namespace Streak\Domain\Event;
53+
// Option 1: Simple Addition (Bus figures out interests)
54+
$bus->add($listener); // The bus *might* inspect the listener or rely on configuration
55+
// to know which events this listener handles.
6356

64-
interface Listener // Often combined with IdempotentListener
65-
{
66-
/**
67-
* Called by the EventBus when a subscribed event occurs.
68-
*
69-
* @param Envelope $event The event envelope.
70-
*/
71-
public function on(Envelope $event): void;
57+
// Option 2: Explicit Subscription (Specific Bus implementations might offer this)
58+
// $bus->subscribe(UserRegistered::class, $listenerServiceId);
59+
// $bus->subscribe(ProductCreated::class, $anotherListener);
7260

73-
/**
74-
* Optional: Called if processing via on() fails.
75-
*
76-
* @param Envelope $event The event envelope that failed.
77-
* @param \Throwable $exception The exception caught during processing.
78-
*/
79-
// public function onError(Envelope $event, \Throwable $exception): void;
61+
// Option 3: Tagging (In frameworks like Symfony)
62+
// Services tagged with 'streak.event_listener' might be automatically registered.
63+
// The framework extension might analyze the listener's method signatures (e.g., on<EventName>).
8064

81-
/**
82-
* Allows the listener to specify which events it's interested in.
83-
* Can return class names, etc., depending on the bus implementation.
84-
*
85-
* @return string[]|callable[]
86-
*/
87-
public function listensTo(): iterable;
88-
}
65+
// Option 4: Manual Dispatch (Less common for buses)
66+
// $listener->on($eventEnvelope); // Bypasses the bus entirely
8967
```
9068

91-
* **`listensTo()`:** Specifies which event types (usually class names) the listener wants to receive. The Event Bus uses this information for routing.
92-
* **`on(Envelope $event)`:** The main method called by the bus when a subscribed event is published. It receives the full `Envelope` containing the event message and metadata.
93-
* **Idempotency:** Listeners should often be **idempotent**, meaning processing the same event multiple times should not result in incorrect state or unintended side effects. This is important because in distributed or asynchronous systems, events might be delivered more than once. Streak provides `Streak\Domain\Event\Listener\IdempotentListener` which usually involves tracking processed event IDs.
69+
The exact registration mechanism depends on the specific `EventBus` implementation and potentially the framework integration (like the [Streak Symfony Bundle](../symfony-bundle/)). Some implementations might require explicit event type subscriptions, while others might introspect the listener (e.g., looking for `on<EventName>` methods or relying on framework tags).
9470

95-
**Example Listener (Conceptual):**
71+
## Example Listener for Event Bus
9672

9773
```php
9874
<?php
9975

100-
namespace My\Application\Listeners;
76+
namespace App\Infrastructure\Notifier;
10177

10278
use Streak\Domain\Event;
103-
use My\Domain\Events;
104-
use MyReadModel\UpdaterService;
105-
106-
// Assuming IdempotentListener checks processed event IDs
107-
class ProjectReadModelUpdater implements Event\Listener // Might be IdempotentListener
79+
use Streak\Domain\Event\Listener;
80+
use Streak\Domain\Event\Listener\Id; // Listeners usually need IDs
81+
use Streak\Domain\Id\UUID;
82+
// Assuming App\Domain\User\Event\UserRegistered exists
83+
use App\Domain\User\Event\UserRegistered;
84+
use Psr\Log\LoggerInterface; // Example dependency
85+
86+
class UserRegistrationNotifier implements Listener, Listener\Id
10887
{
109-
public function __construct(private UpdaterService $updater) {}
110-
111-
public function listensTo(): iterable
112-
{
113-
return [
114-
Events\ProjectCreated::class,
115-
Events\ProjectRenamed::class,
116-
Events\TaskAddedToProject::class,
117-
];
88+
use Event\Listener\Identifying; // Trait for ID handling
89+
// Note: Listening trait is optional here; can implement on() directly
90+
91+
public function __construct(
92+
private LoggerInterface $logger,
93+
// Listener IDs are typically generated or assigned externally
94+
?Listener\Id $id = null
95+
) {
96+
$this->identifyBy($id ?? new class extends UUID implements Listener\Id {});
11897
}
11998

120-
public function on(Event\Envelope $envelope): void
99+
public function listenerId(): Listener\Id { return $this->id; }
100+
101+
/**
102+
* Called by the Event Bus when an event occurs.
103+
*/
104+
public function on(Event\Envelope $envelope): bool
121105
{
122106
$event = $envelope->message();
123107

124-
match ($event::class) {
125-
Events\ProjectCreated::class => $this->updater->createProjectEntry($event->projectId(), $event->projectName()),
126-
Events\ProjectRenamed::class => $this->updater->updateProjectName($event->projectId(), $event->projectName()),
127-
Events\TaskAddedToProject::class => $this->updater->addTaskToProject($event->projectId(), $event->taskId(), $event->taskName()),
128-
default => // Ignore other events
129-
};
108+
// Check if it's the event we care about
109+
if ($event instanceof UserRegistered) {
110+
$this->logger->info(sprintf('User registered: %s', $event->userId));
111+
// In a real app: send email, push notification, etc.
112+
113+
return true; // Event handled
114+
}
115+
116+
return false; // Event ignored
130117
}
131118
}
132119
```
120+
* **`on()`:** Contains the logic to handle the specific event (`UserRegistered` in this case). It checks the event type and returns `true` if handled.
121+
* **`Listener\Id` / `Identifying` Trait:** Included because listeners often need IDs, even if just for logging or potential future state management, although simpler transient listeners might omit it if the bus implementation allows.
133122

134-
## Implementations
123+
## Event Bus Implementations
135124

136125
Streak core might provide basic synchronous implementations. More advanced implementations often integrate with message queuing systems (like RabbitMQ, Kafka) or use frameworks like Symfony Messenger.
137126

138-
* **Synchronous:** The `publish` method directly calls `on` for all subscribed listeners within the same process and request. Simple but can slow down the initial request.
127+
* **Synchronous:** The `publish` method directly calls `on` for all registered listeners within the same process and request. Simple but can slow down the initial request.
139128
* **Asynchronous:** The `publish` method sends the event envelope to a message queue. Separate worker processes consume messages from the queue and invoke the appropriate listeners.
140129

141130
## Integration with Event Store
142131

143-
As mentioned in the Event Store documentation, the `Streak\Infrastructure\Domain\EventStore\PublishingEventStore` decorator is key. It ensures that `eventBus->publish()` is only called *after* the event has been successfully committed to the underlying persistent event store (`eventStore->add()`). This prevents listeners reacting via the bus to events that might ultimately fail to be persisted.
132+
As mentioned in the [Event Store](./event-store.md) documentation, the `Streak\Infrastructure\Domain\EventStore\PublishingEventStore` decorator is key. It ensures that `eventBus->publish()` is only called *after* the event has been successfully committed to the underlying persistent event store (`eventStore->add()`). This prevents listeners reacting via the bus to events that might ultimately fail to be persisted.
144133

145-
## Configuration
134+
## Event Bus vs. Subscriptions
146135

147-
Event Bus implementations and listener registration (subscribing listeners to the bus) are typically configured via dependency injection containers. The `StreakBundle` for Symfony automates the discovery and registration of services implementing `Event\Listener` with the configured Event Bus service.
136+
While both Event Buses and [Subscriptions](./subscriptions.md) involve processing events with Listeners, they serve different primary purposes:
148137

149-
**Note on Direct Usage:** While the `EventBus` interface defines `publish` and `subscribe` methods, in many common Streak setups (especially using the `StreakBundle` and the `PublishingEventStore`), direct interaction with the event bus by the application developer is often minimal.
150-
* The `PublishingEventStore` automatically handles calling `publish()` after events are successfully persisted.
151-
* Listeners implementing `Streak\Domain\Event\Listener` are typically auto-discovered and subscribed to the bus by the framework's dependency injection container (autoconfiguration).
152-
As a result, developers usually focus on creating the listener logic (the `on()` methods) and defining which events they `listenTo()`, rather than manually publishing events or subscribing listeners to the bus.
138+
* **Event Bus:** Focuses on **immediate, decoupled notification**. Ideal for simple side effects that need to happen *soon* after an event but don't necessarily require guaranteed, ordered processing relative to other events (e.g., sending a welcome email). Often asynchronous.
139+
* **Subscription:** Focuses on **reliable, persistent, often ordered processing** of the event stream, typically starting from a specific point. Essential for building stateful read models (Projections) or managing long-running business processes (Process Managers/Sagas) that depend on the sequence of events.
153140

154-
## Event Bus vs. Subscriptions
141+
In many systems, both are used: an event is persisted to the Event Store, then published to the Event Bus for immediate notifications, *and* processed later by one or more Subscriptions for building read models or driving processes.

0 commit comments

Comments
 (0)