diff --git a/public/docs/packages/event/interfaces/action-binding-strategy.md b/public/docs/packages/event/interfaces/action-binding-strategy.md new file mode 100644 index 0000000..e2f7c68 --- /dev/null +++ b/public/docs/packages/event/interfaces/action-binding-strategy.md @@ -0,0 +1,433 @@ +--- +id: event-interface-action-binding-strategy +slug: docs/packages/event/interfaces/action-binding-strategy +title: ActionBindingStrategy Interface +doc_type: reference +status: active +language: en +owner: docs-team +last_reviewed: 2026-01-25 +applies_to: ["all"] +canonical: true +summary: The ActionBindingStrategy interface binds external platform actions to internal application events. +llm_summary: > + ActionBindingStrategy bridges external platform actions (like WordPress hooks) to internal events + in phpnomad/event. The bindAction() method registers a listener on an external action that creates + and broadcasts an internal event when triggered. Accepts an event class, the external action name, + and an optional transformer to convert platform data into event constructor arguments. Used by + wordpress-integration to connect WordPress actions to PHPNomad events without coupling application + code to WordPress. +questions_answered: + - What is ActionBindingStrategy? + - How do I connect WordPress hooks to my events? + - What is the transformer parameter for? + - How does ActionBindingStrategy work with EventStrategy? +audience: + - developers + - backend engineers +tags: + - events + - interface + - reference + - platform-integration +llm_tags: + - action-binding-strategy + - platform-bridge + - wordpress-hooks +keywords: + - ActionBindingStrategy + - bindAction + - platform hooks + - WordPress integration +related: + - introduction + - has-event-bindings + - event-strategy +see_also: + - ../introduction + - ../../wordpress-integration/introduction +noindex: false +--- + +# ActionBindingStrategy Interface + +The `ActionBindingStrategy` interface connects external platform actions (like WordPress hooks) to your internal event system. + +--- + +## Interface Definition + +```php +namespace PHPNomad\Events\Interfaces; + +interface ActionBindingStrategy +{ + /** + * Binds an external action to an event class. + * + * @param string $eventClass The event class to create + * @param string $actionToBind The external action to listen for + * @param callable|null $transformer Converts action args to event constructor args + */ + public function bindAction(string $eventClass, string $actionToBind, ?callable $transformer = null); +} +``` + +--- + +## Method Reference + +### `bindAction(string $eventClass, string $actionToBind, ?callable $transformer = null)` + +Registers a binding between an external action and an internal event. + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$eventClass` | `string` | Fully-qualified class name of the Event to create | +| `$actionToBind` | `string` | Name of the external action/hook to listen for | +| `$transformer` | `?callable` | Converts action arguments to event constructor arguments | + +**Flow:** +``` +External Action fires + │ + ▼ +ActionBindingStrategy intercepts + │ + ▼ +Transformer converts arguments + │ + ▼ +Event instance created + │ + ▼ +EventStrategy broadcasts + │ + ▼ +Your handlers respond +``` + +--- + +## Basic Usage + +### Without Transformer + +When the external action provides arguments matching your event constructor: + +```php +// Event expects ($userId) +// WordPress 'user_register' hook provides ($userId) +$binding->bindAction( + UserCreatedEvent::class, + 'user_register' +); +``` + +### With Transformer + +When arguments need conversion: + +```php +$binding->bindAction( + OrderPlacedEvent::class, + 'woocommerce_new_order', + function($orderId) { + $order = wc_get_order($orderId); + return [ + $orderId, + $order->get_customer_id(), + (float) $order->get_total(), + ]; + } +); +``` + +The transformer returns an array of arguments for the event constructor. + +--- + +## How It Works + +### Conceptual Implementation + +```php +class WordPressActionBindingStrategy implements ActionBindingStrategy +{ + public function __construct(private EventStrategy $events) {} + + public function bindAction( + string $eventClass, + string $actionToBind, + ?callable $transformer = null + ) { + add_action($actionToBind, function(...$args) use ($eventClass, $transformer) { + // Transform arguments if transformer provided + $eventArgs = $transformer ? $transformer(...$args) : $args; + + // Create event instance + $event = new $eventClass(...$eventArgs); + + // Broadcast through internal event system + $this->events->broadcast($event); + }); + } +} +``` + +--- + +## Usage Patterns + +### Binding at Bootstrap + +```php +class WordPressBootstrapper +{ + public function __construct( + private ActionBindingStrategy $bindings + ) {} + + public function boot(): void + { + // User events + $this->bindings->bindAction( + UserCreatedEvent::class, + 'user_register', + fn($userId) => [$userId, get_userdata($userId)->user_email] + ); + + // Post events + $this->bindings->bindAction( + PostPublishedEvent::class, + 'publish_post', + fn($postId, $post) => [$postId, $post->post_title] + ); + } +} +``` + +### Processing HasEventBindings + +`ActionBindingStrategy` is often used to process `HasEventBindings` declarations: + +```php +class EventBindingProcessor +{ + public function __construct( + private ActionBindingStrategy $bindings + ) {} + + public function process(HasEventBindings $provider): void + { + foreach ($provider->getEventBindings() as $eventClass => $configs) { + foreach ($configs as $config) { + $this->bindings->bindAction( + $eventClass, + $config['action'], + $config['transformer'] ?? null + ); + } + } + } +} +``` + +--- + +## Transformer Patterns + +### Identity Transformer (Pass-through) + +When action args match event constructor: + +```php +// No transformer needed +$binding->bindAction(SimpleEvent::class, 'simple_action'); + +// Explicit pass-through +$binding->bindAction( + SimpleEvent::class, + 'simple_action', + fn(...$args) => $args +); +``` + +### Extracting Data + +```php +$binding->bindAction( + CustomerCreatedEvent::class, + 'woocommerce_created_customer', + function($customerId, $newCustomerData, $passwordGenerated) { + // Only pass what the event needs + return [ + $customerId, + $newCustomerData['user_email'], + ]; + } +); +``` + +### Enriching Data + +```php +$binding->bindAction( + PostPublishedEvent::class, + 'publish_post', + function($postId, $post) { + $author = get_userdata($post->post_author); + return [ + $postId, + $post->post_title, + $author->user_email, + new \DateTimeImmutable($post->post_date), + ]; + } +); +``` + +### Conditional Binding + +Return `null` from transformer to skip event creation: + +```php +$binding->bindAction( + ProductPublishedEvent::class, + 'save_post', + function($postId, $post, $update) { + // Only for new products, not updates + if ($update || $post->post_type !== 'product') { + return null; + } + return [$postId, $post->post_title]; + } +); +``` + +--- + +## Real-World Example + +### Complete WordPress Integration + +```php +class WordPressEventBridge +{ + public function __construct( + private ActionBindingStrategy $bindings + ) {} + + public function register(): void + { + $this->registerUserBindings(); + $this->registerPostBindings(); + $this->registerCommentBindings(); + } + + private function registerUserBindings(): void + { + // User registration + $this->bindings->bindAction( + UserRegisteredEvent::class, + 'user_register', + function($userId) { + $user = get_userdata($userId); + return [ + $userId, + $user->user_email, + $user->user_login, + ]; + } + ); + + // Profile update + $this->bindings->bindAction( + UserProfileUpdatedEvent::class, + 'profile_update', + fn($userId, $oldData) => [$userId, $oldData] + ); + + // User deletion + $this->bindings->bindAction( + UserDeletedEvent::class, + 'delete_user', + fn($userId, $reassignId) => [$userId] + ); + } + + private function registerPostBindings(): void + { + // New post published + $this->bindings->bindAction( + PostPublishedEvent::class, + 'publish_post', + fn($postId, $post) => [$postId, $post->post_title, $post->post_author] + ); + + // Post status changed + $this->bindings->bindAction( + PostStatusChangedEvent::class, + 'transition_post_status', + fn($new, $old, $post) => [$post->ID, $old, $new] + ); + } + + private function registerCommentBindings(): void + { + $this->bindings->bindAction( + CommentPostedEvent::class, + 'wp_insert_comment', + function($commentId, $comment) { + return [ + $commentId, + $comment->comment_post_ID, + $comment->comment_author_email, + ]; + } + ); + } +} +``` + +--- + +## Testing + +Mock `ActionBindingStrategy` to verify bindings are registered: + +```php +class WordPressEventBridgeTest extends TestCase +{ + public function test_registers_user_bindings(): void + { + $bindings = $this->createMock(ActionBindingStrategy::class); + + $bindings->expects($this->atLeastOnce()) + ->method('bindAction') + ->with( + $this->equalTo(UserRegisteredEvent::class), + $this->equalTo('user_register'), + $this->isType('callable') + ); + + $bridge = new WordPressEventBridge($bindings); + $bridge->register(); + } +} +``` + +--- + +## Related Interfaces + +- **[HasEventBindings](has-event-bindings)** — Declarative binding configuration +- **[EventStrategy](event-strategy)** — Receives broadcasted events +- **[Event](event)** — Events created from bindings + +--- + +## Further Reading + +- **[Event Bindings Guide](/core-concepts/bootstrapping/initializers/event-binding)** — Tutorial-style guide with WordPress examples +- **[Best Practices](../patterns/best-practices)** — Transformer patterns and testing strategies diff --git a/public/docs/packages/event/interfaces/can-handle.md b/public/docs/packages/event/interfaces/can-handle.md new file mode 100644 index 0000000..772036a --- /dev/null +++ b/public/docs/packages/event/interfaces/can-handle.md @@ -0,0 +1,372 @@ +--- +id: event-interface-can-handle +slug: docs/packages/event/interfaces/can-handle +title: CanHandle Interface +doc_type: reference +status: active +language: en +owner: docs-team +last_reviewed: 2026-01-25 +applies_to: ["all"] +canonical: true +summary: The CanHandle interface defines event handler classes that respond to specific events. +llm_summary: > + CanHandle is a generic interface for event handler classes in phpnomad/event. Handlers implement + handle(Event $event): void to respond when their associated event is broadcast. The interface + uses PHP generics (@template T of Event) for type safety. Handlers are typically registered via + HasListeners and instantiated through the DI container, enabling dependency injection of services + like email, logging, or database access. Keep handlers focused on a single responsibility. +questions_answered: + - What is the CanHandle interface? + - How do I create an event handler? + - How do handlers get dependencies? + - Can one handler handle multiple events? + - How do I access event data in a handler? +audience: + - developers + - backend engineers +tags: + - events + - interface + - reference + - handlers +llm_tags: + - can-handle + - event-handler + - handler-pattern +keywords: + - CanHandle + - event handler + - handle method +related: + - introduction + - event + - has-listeners +see_also: + - ../introduction + - ../patterns/best-practices +noindex: false +--- + +# CanHandle Interface + +The `CanHandle` interface defines event handler classes—dedicated objects that respond when specific events are broadcast. + +--- + +## Interface Definition + +```php +namespace PHPNomad\Events\Interfaces; + +/** + * @template T of Event + */ +interface CanHandle +{ + /** + * Handle the event. + * + * @param T $event + */ + public function handle(Event $event): void; +} +``` + +--- + +## Method Reference + +### `handle(Event $event): void` + +Called when the associated event is broadcast. + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$event` | `Event` | The event object containing data about what happened | +| **Returns** | `void` | | + +--- + +## Creating Handlers + +### Basic Handler + +```php +use PHPNomad\Events\Interfaces\CanHandle; +use PHPNomad\Events\Interfaces\Event; + +/** + * @implements CanHandle + */ +class LogUserCreationHandler implements CanHandle +{ + public function handle(Event $event): void + { + /** @var UserCreatedEvent $event */ + error_log("User created: {$event->userId}"); + } +} +``` + +### Handler with Dependencies + +Handlers are instantiated through the DI container, enabling dependency injection: + +```php +/** + * @implements CanHandle + */ +class SendWelcomeEmailHandler implements CanHandle +{ + public function __construct( + private EmailStrategy $email, + private TemplateRenderer $templates + ) {} + + public function handle(Event $event): void + { + /** @var UserCreatedEvent $event */ + $html = $this->templates->render('welcome', [ + 'userId' => $event->userId, + 'email' => $event->email, + ]); + + $this->email->send( + to: $event->email, + subject: 'Welcome!', + body: $html + ); + } +} +``` + +### Handler with Error Handling + +```php +/** + * @implements CanHandle + */ +class ProcessPaymentHandler implements CanHandle +{ + public function __construct( + private PaymentProcessor $processor, + private LoggerStrategy $logger + ) {} + + public function handle(Event $event): void + { + /** @var PaymentReceivedEvent $event */ + try { + $this->processor->confirm($event->transactionId); + } catch (PaymentException $e) { + $this->logger->error('Payment confirmation failed', [ + 'transaction_id' => $event->transactionId, + 'error' => $e->getMessage(), + ]); + // Decide: re-throw, queue for retry, or swallow + } + } +} +``` + +--- + +## Registering Handlers + +Handlers are typically registered via [HasListeners](has-listeners): + +```php +class MyModule implements HasListeners +{ + public function getListeners(): array + { + return [ + UserCreatedEvent::class => SendWelcomeEmailHandler::class, + OrderPlacedEvent::class => [ + SendOrderConfirmationHandler::class, + UpdateInventoryHandler::class, + NotifyWarehouseHandler::class, + ], + ]; + } +} +``` + +--- + +## Type Safety with Generics + +The `@template` annotation provides IDE support and static analysis: + +```php +/** + * @implements CanHandle + */ +class NotifyCustomerOfShipmentHandler implements CanHandle +{ + public function handle(Event $event): void + { + // IDE knows $event is OrderShippedEvent + $trackingNumber = $event->trackingNumber; + $carrier = $event->carrier; + } +} +``` + +**Without the annotation**, you need explicit type assertions: + +```php +public function handle(Event $event): void +{ + /** @var OrderShippedEvent $event */ + // or + assert($event instanceof OrderShippedEvent); +} +``` + +--- + +## Best Practices + +### 1. One Handler, One Responsibility + +Each handler should do one thing: + +```php +// Good: focused handlers +class SendWelcomeEmailHandler implements CanHandle { /* sends email */ } +class CreateUserProfileHandler implements CanHandle { /* creates profile */ } +class NotifyAdminOfNewUserHandler implements CanHandle { /* notifies admin */ } + +// Bad: handler does too much +class UserCreatedHandler implements CanHandle +{ + public function handle(Event $event): void + { + $this->sendWelcomeEmail($event); + $this->createUserProfile($event); + $this->notifyAdmin($event); + $this->updateStatistics($event); + } +} +``` + +### 2. Inject Dependencies + +Let the container manage dependencies: + +```php +// Good: dependencies injected +class NotifySlackHandler implements CanHandle +{ + public function __construct(private SlackClient $slack) {} +} + +// Bad: creates own dependencies +class NotifySlackHandler implements CanHandle +{ + public function handle(Event $event): void + { + $slack = new SlackClient(getenv('SLACK_TOKEN')); // Hard to test + } +} +``` + +### 3. Keep Handlers Fast + +Long-running operations should be queued: + +```php +// Good: queue heavy work +class GenerateReportHandler implements CanHandle +{ + public function __construct(private Queue $queue) {} + + public function handle(Event $event): void + { + $this->queue->push(new GenerateReportJob($event->reportId)); + } +} + +// Bad: blocks event dispatch +class GenerateReportHandler implements CanHandle +{ + public function handle(Event $event): void + { + $this->generateReport($event->reportId); // Takes 5 minutes! + } +} +``` + +### 4. Handle Errors Gracefully + +Don't let one handler break others: + +```php +public function handle(Event $event): void +{ + try { + $this->doWork($event); + } catch (\Exception $e) { + $this->logger->error('Handler failed', [ + 'handler' => self::class, + 'event' => $event->getId(), + 'error' => $e->getMessage(), + ]); + // Don't re-throw unless you want to stop other handlers + } +} +``` + +--- + +## Testing Handlers + +Handlers are easy to test because they're focused classes with injected dependencies: + +```php +class SendWelcomeEmailHandlerTest extends TestCase +{ + public function test_sends_welcome_email(): void + { + $email = $this->createMock(EmailStrategy::class); + $templates = $this->createMock(TemplateRenderer::class); + + $templates->method('render') + ->with('welcome', ['userId' => 123, 'email' => 'test@example.com']) + ->willReturn('Welcome!'); + + $email->expects($this->once()) + ->method('send') + ->with( + to: 'test@example.com', + subject: 'Welcome!', + body: 'Welcome!' + ); + + $handler = new SendWelcomeEmailHandler($email, $templates); + $handler->handle(new UserCreatedEvent( + userId: 123, + email: 'test@example.com', + createdAt: new \DateTimeImmutable() + )); + } +} +``` + +--- + +## Related Interfaces + +- **[Event](event)** — Events that handlers receive +- **[EventStrategy](event-strategy)** — Dispatcher that calls handlers +- **[HasListeners](has-listeners)** — Declares which handlers respond to which events + +--- + +## Further Reading + +- **[Event Listeners Guide](/core-concepts/bootstrapping/initializers/event-listeners)** — Tutorial-style guide with dependency injection examples +- **[Best Practices](../patterns/best-practices)** — Handler patterns, testing strategies, anti-patterns +- **[Logger Package](../../logger/introduction.md)** — LoggerStrategy for logging in handlers diff --git a/public/docs/packages/event/interfaces/event-strategy.md b/public/docs/packages/event/interfaces/event-strategy.md new file mode 100644 index 0000000..ff275cb --- /dev/null +++ b/public/docs/packages/event/interfaces/event-strategy.md @@ -0,0 +1,402 @@ +--- +id: event-interface-event-strategy +slug: docs/packages/event/interfaces/event-strategy +title: EventStrategy Interface +doc_type: reference +status: active +language: en +owner: docs-team +last_reviewed: 2026-01-25 +applies_to: ["all"] +canonical: true +summary: The EventStrategy interface defines the core event dispatcher for broadcasting events and managing listeners. +llm_summary: > + EventStrategy is the central interface for event management in phpnomad/event. It provides three + methods: broadcast() dispatches an Event to all registered listeners, attach() registers a callable + to respond to a specific event ID with optional priority, and detach() removes a previously attached + listener. Implementations include symfony-event-dispatcher-integration. Priority determines execution + order (higher priority runs first). This is the interface you inject when you need to dispatch events. +questions_answered: + - What is EventStrategy? + - How do I broadcast events? + - How do I attach listeners? + - How does priority work? + - How do I detach listeners? + - What implementations are available? +audience: + - developers + - backend engineers +tags: + - events + - interface + - reference + - dispatcher +llm_tags: + - event-strategy + - event-dispatcher + - broadcast +keywords: + - EventStrategy + - broadcast + - attach + - detach + - event dispatcher +related: + - introduction + - event + - can-handle +see_also: + - ../introduction + - ../../symfony-event-dispatcher-integration/introduction +noindex: false +--- + +# EventStrategy Interface + +The `EventStrategy` interface is the core dispatcher in PHPNomad's event system. It handles broadcasting events to listeners and managing listener registration. + +--- + +## Interface Definition + +```php +namespace PHPNomad\Events\Interfaces; + +interface EventStrategy +{ + /** + * Broadcasts an event to all attached listeners. + */ + public function broadcast(Event $event): void; + + /** + * Attaches a listener to an event. + */ + public function attach(string $event, callable $action, ?int $priority = null): void; + + /** + * Detaches a listener from an event. + */ + public function detach(string $event, callable $action, ?int $priority = null): void; +} +``` + +--- + +## Method Reference + +### `broadcast(Event $event): void` + +Dispatches an event to all listeners registered for that event's ID. + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$event` | `Event` | The event object to broadcast | +| **Returns** | `void` | | + +```php +$events->broadcast(new UserCreatedEvent($user)); +``` + +The event's `getId()` method determines which listeners are called. + +--- + +### `attach(string $event, callable $action, ?int $priority = null): void` + +Registers a listener for an event ID. + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$event` | `string` | Event ID to listen for | +| `$action` | `callable` | Function to call when event fires | +| `$priority` | `?int` | Execution order (higher = earlier). Default varies by implementation | +| **Returns** | `void` | | + +```php +// Attach a closure +$events->attach('user.created', function(UserCreatedEvent $event) { + // Handle the event +}); + +// Attach a method +$events->attach('user.created', [$this, 'onUserCreated']); + +// Attach with priority +$events->attach('user.created', $handler, priority: 100); +``` + +--- + +### `detach(string $event, callable $action, ?int $priority = null): void` + +Removes a previously attached listener. + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$event` | `string` | Event ID to stop listening for | +| `$action` | `callable` | The exact callable that was attached | +| `$priority` | `?int` | Priority it was attached with | +| **Returns** | `void` | | + +```php +// Remove a listener +$events->detach('user.created', $myHandler); +``` + +**Note:** You must pass the exact same callable reference that was used in `attach()`. + +--- + +## Priority System + +Listeners can specify a priority that determines execution order. + +```php +// High priority (runs first) +$events->attach('order.placed', $validateHandler, priority: 100); + +// Default priority +$events->attach('order.placed', $saveHandler, priority: 50); + +// Low priority (runs last) +$events->attach('order.placed', $notifyHandler, priority: 10); +``` + +**Execution order:** 100 → 50 → 10 (highest to lowest) + +### Priority Guidelines + +| Priority Range | Use Case | +|----------------|----------| +| 90-100 | Validation, security checks | +| 50-89 | Core business logic | +| 10-49 | Side effects, notifications | +| 1-9 | Logging, cleanup | + +--- + +## Usage Patterns + +### Injecting EventStrategy + +Use dependency injection to get an `EventStrategy` instance: + +```php +class OrderService +{ + public function __construct( + private EventStrategy $events, + private OrderRepository $orders + ) {} + + public function placeOrder(Cart $cart): Order + { + $order = Order::fromCart($cart); + $this->orders->save($order); + + $this->events->broadcast(new OrderPlacedEvent( + orderId: $order->getId(), + total: $order->getTotal() + )); + + return $order; + } +} +``` + +### Registering Listeners at Bootstrap + +```php +class AppServiceProvider +{ + public function __construct( + private EventStrategy $events, + private Container $container + ) {} + + public function boot(): void + { + // Closure listener + $this->events->attach('user.created', function($event) { + // Handle event + }); + + // Handler class (resolved through container) + $this->events->attach('user.created', function($event) { + $this->container->get(SendWelcomeEmailHandler::class)->handle($event); + }); + } +} +``` + +### Conditional Event Handling + +```php +$this->events->attach('post.published', function(PostPublishedEvent $event) { + // Only notify for featured posts + if ($event->isFeatured) { + $this->notifier->notifySubscribers($event->postId); + } +}); +``` + +--- + +## Implementations + +### Symfony EventDispatcher Integration + +The primary implementation uses Symfony's EventDispatcher: + +```bash +composer require phpnomad/symfony-event-dispatcher-integration +``` + +See [Symfony Event Dispatcher Integration](/packages/symfony-event-dispatcher-integration/introduction) for details. + +### Custom Implementation + +You can create your own implementation: + +```php +class SimpleEventStrategy implements EventStrategy +{ + private array $listeners = []; + + public function broadcast(Event $event): void + { + $id = $event->getId(); + + if (!isset($this->listeners[$id])) { + return; + } + + // Sort by priority (descending) + $listeners = $this->listeners[$id]; + usort($listeners, fn($a, $b) => $b['priority'] <=> $a['priority']); + + foreach ($listeners as $listener) { + ($listener['action'])($event); + } + } + + public function attach(string $event, callable $action, ?int $priority = null): void + { + $this->listeners[$event][] = [ + 'action' => $action, + 'priority' => $priority ?? 0, + ]; + } + + public function detach(string $event, callable $action, ?int $priority = null): void + { + if (!isset($this->listeners[$event])) { + return; + } + + $this->listeners[$event] = array_filter( + $this->listeners[$event], + fn($l) => $l['action'] !== $action + ); + } +} +``` + +--- + +## Testing with EventStrategy + +### Mocking in Tests + +```php +class OrderServiceTest extends TestCase +{ + public function test_broadcast_event_on_order_placed(): void + { + $events = $this->createMock(EventStrategy::class); + + $events->expects($this->once()) + ->method('broadcast') + ->with($this->callback(fn($e) => + $e instanceof OrderPlacedEvent && + $e->orderId === 123 + )); + + $service = new OrderService($events, $this->orders); + $service->placeOrder($this->cart); + } +} +``` + +### Capturing Broadcasted Events + +```php +class TestEventStrategy implements EventStrategy +{ + public array $broadcastedEvents = []; + + public function broadcast(Event $event): void + { + $this->broadcastedEvents[] = $event; + } + + // ... attach/detach implementations +} + +// In test +$events = new TestEventStrategy(); +$service = new OrderService($events, $orders); +$service->placeOrder($cart); + +$this->assertCount(1, $events->broadcastedEvents); +$this->assertInstanceOf(OrderPlacedEvent::class, $events->broadcastedEvents[0]); +``` + +--- + +## Common Mistakes + +### Forgetting to Broadcast + +```php +// Bad: event created but never broadcast +public function createUser(): User +{ + $user = new User(); + $this->users->save($user); + new UserCreatedEvent($user); // Lost! + return $user; +} + +// Good: actually broadcast +public function createUser(): User +{ + $user = new User(); + $this->users->save($user); + $this->events->broadcast(new UserCreatedEvent($user)); + return $user; +} +``` + +### Blocking in Listeners + +```php +// Bad: slow listener blocks the broadcast +$events->attach('order.placed', function($event) { + $this->sendEmailToAllSubscribers($event); // Takes 30 seconds! +}); + +// Good: queue heavy work +$events->attach('order.placed', function($event) { + $this->queue->push(new SendOrderEmailsJob($event->orderId)); +}); +``` + +--- + +## Related Interfaces + +- **[Event](event)** — Events that are broadcast +- **[CanHandle](can-handle)** — Handler classes for events +- **[HasListeners](has-listeners)** — Declarative listener registration diff --git a/public/docs/packages/event/interfaces/event.md b/public/docs/packages/event/interfaces/event.md new file mode 100644 index 0000000..16dde29 --- /dev/null +++ b/public/docs/packages/event/interfaces/event.md @@ -0,0 +1,357 @@ +--- +id: event-interface-event +slug: docs/packages/event/interfaces/event +title: Event Interface +doc_type: reference +status: active +language: en +owner: docs-team +last_reviewed: 2026-01-25 +applies_to: ["all"] +canonical: true +summary: The Event interface defines the contract for all event objects in PHPNomad's event system. +llm_summary: > + The Event interface is the base contract for all events in phpnomad/event. It requires a single + static method getId() that returns a unique string identifier for the event type. This ID is used + by EventStrategy to match events to their listeners. Events should be immutable value objects + that carry data about something that happened. Common patterns include using class constants or + fully-qualified class names as IDs. +questions_answered: + - What is the Event interface? + - How do I create an event class? + - What should getId() return? + - Should events be mutable or immutable? + - How do I include data in an event? +audience: + - developers + - backend engineers +tags: + - events + - interface + - reference +llm_tags: + - event-interface + - event-creation +keywords: + - Event interface + - getId + - event class +related: + - introduction + - event-strategy + - can-handle +see_also: + - ../introduction + - ../patterns/best-practices +noindex: false +--- + +# Event Interface + +The `Event` interface is the foundation of PHPNomad's event system. Every event class must implement this interface to be broadcastable through `EventStrategy`. + +--- + +## Interface Definition + +```php +namespace PHPNomad\Events\Interfaces; + +interface Event +{ + /** + * Returns the unique identifier for this event type. + * + * @return string The event identifier + */ + public static function getId(): string; +} +``` + +--- + +## Method Reference + +### `getId(): string` + +Returns a unique string identifier for the event type. + +| Aspect | Details | +|--------|---------| +| Visibility | `public static` | +| Parameters | None | +| Returns | `string` — Unique event identifier | +| Called by | `EventStrategy` when matching events to listeners | + +**Important:** This is a static method. The ID identifies the *type* of event, not a specific instance. + +--- + +## Creating Event Classes + +### Basic Event + +The simplest event just implements the interface: + +```php +use PHPNomad\Events\Interfaces\Event; + +class UserLoggedInEvent implements Event +{ + public static function getId(): string + { + return 'user.logged_in'; + } +} +``` + +### Event with Data + +Events typically carry data about what happened: + +```php +use PHPNomad\Events\Interfaces\Event; + +class UserCreatedEvent implements Event +{ + public function __construct( + public readonly int $userId, + public readonly string $email, + public readonly \DateTimeImmutable $createdAt + ) {} + + public static function getId(): string + { + return 'user.created'; + } +} +``` + +### Using Class Name as ID + +A common pattern uses the fully-qualified class name: + +```php +class OrderPlacedEvent implements Event +{ + public static function getId(): string + { + return self::class; + } +} +``` + +This guarantees uniqueness but produces longer IDs like `App\Events\OrderPlacedEvent`. + +--- + +## Event ID Patterns + +### Dot-notation (Recommended) + +```php +public static function getId(): string +{ + return 'user.created'; +} +``` + +Benefits: +- Short, readable +- Natural grouping (`user.*`, `order.*`) +- Easy to type when attaching listeners + +### Class Name + +```php +public static function getId(): string +{ + return self::class; +} +``` + +Benefits: +- Guaranteed unique +- Refactoring-safe with IDE support + +### Constant + +```php +class UserCreatedEvent implements Event +{ + public const ID = 'user.created'; + + public static function getId(): string + { + return self::ID; + } +} + +// Listeners can reference the constant +$events->attach(UserCreatedEvent::ID, $handler); +``` + +--- + +## Best Practices + +### 1. Make Events Immutable + +Use `readonly` properties (PHP 8.1+) or private properties with getters: + +```php +// PHP 8.1+ with readonly +class PaymentReceivedEvent implements Event +{ + public function __construct( + public readonly string $transactionId, + public readonly float $amount, + public readonly \DateTimeImmutable $receivedAt + ) {} + + public static function getId(): string + { + return 'payment.received'; + } +} +``` + +### 2. Use Past Tense + +Events represent things that already happened: + +```php +// Good: past tense +class OrderShippedEvent {} +class UserDeletedEvent {} +class PaymentFailedEvent {} + +// Bad: present/future tense (sounds like commands) +class ShipOrderEvent {} +class DeleteUserEvent {} +``` + +### 3. Include Sufficient Context + +Events should carry all data handlers need: + +```php +// Good: handlers have everything they need +class OrderCompletedEvent implements Event +{ + public function __construct( + public readonly int $orderId, + public readonly int $customerId, + public readonly float $total, + public readonly array $itemIds, + public readonly string $shippingMethod + ) {} +} + +// Bad: handlers must query for additional data +class OrderCompletedEvent implements Event +{ + public function __construct( + public readonly int $orderId + ) {} +} +``` + +### 4. Use Value Objects for Complex Data + +```php +class ShipmentDispatchedEvent implements Event +{ + public function __construct( + public readonly int $orderId, + public readonly Address $shippingAddress, // Value object + public readonly Carrier $carrier, // Value object + public readonly \DateTimeImmutable $dispatchedAt + ) {} +} +``` + +--- + +## Usage Examples + +### Broadcasting an Event + +```php +$event = new UserCreatedEvent( + userId: 123, + email: 'user@example.com', + createdAt: new \DateTimeImmutable() +); + +$eventStrategy->broadcast($event); +``` + +### Accessing Event Data in Handlers + +```php +class SendWelcomeEmailHandler implements CanHandle +{ + public function handle(Event $event): void + { + /** @var UserCreatedEvent $event */ + $this->emailService->send( + to: $event->email, + subject: 'Welcome!', + template: 'welcome', + data: ['userId' => $event->userId] + ); + } +} +``` + +--- + +## Common Mistakes + +### Mutable Events + +```php +// Bad: mutable state +class UserUpdatedEvent implements Event +{ + public string $email; // Can be modified by handlers! +} + +// Good: immutable +class UserUpdatedEvent implements Event +{ + public function __construct( + public readonly string $email + ) {} +} +``` + +### Missing Data + +```php +// Bad: insufficient context +class InvoiceSentEvent implements Event +{ + public function __construct(public readonly int $invoiceId) {} +} + +// Handlers need customer email but must query for it +class NotifyCustomerHandler implements CanHandle +{ + public function handle(Event $event): void + { + $invoice = $this->invoices->find($event->invoiceId); + $customer = $this->customers->find($invoice->customerId); + // Now we can finally send the email + } +} +``` + +--- + +## Related Interfaces + +- **[EventStrategy](event-strategy)** — Broadcasts events to listeners +- **[CanHandle](can-handle)** — Handles events when they're broadcast +- **[HasListeners](has-listeners)** — Declares which events a module listens to diff --git a/public/docs/packages/event/interfaces/has-event-bindings.md b/public/docs/packages/event/interfaces/has-event-bindings.md new file mode 100644 index 0000000..16f131a --- /dev/null +++ b/public/docs/packages/event/interfaces/has-event-bindings.md @@ -0,0 +1,422 @@ +--- +id: event-interface-has-event-bindings +slug: docs/packages/event/interfaces/has-event-bindings +title: HasEventBindings Interface +doc_type: reference +status: active +language: en +owner: docs-team +last_reviewed: 2026-01-25 +applies_to: ["all"] +canonical: true +summary: The HasEventBindings interface provides flexible event binding configuration for platform integration. +llm_summary: > + HasEventBindings provides flexible event binding configuration in phpnomad/event. Unlike HasListeners + which maps events to handlers, HasEventBindings returns an array of binding configurations that can + include additional metadata like platform actions and transformers. Primary use case is bridging + platform-specific events (like WordPress hooks) to application events. Each binding specifies an + event class, an external action to listen for, and a transformer function that converts platform + data into the application event. +questions_answered: + - What is HasEventBindings? + - How is HasEventBindings different from HasListeners? + - How do I bind platform events to my application? + - What is an event transformer? + - When should I use HasEventBindings? +audience: + - developers + - backend engineers +tags: + - events + - interface + - reference + - platform-integration +llm_tags: + - has-event-bindings + - platform-bridge + - event-transformer +keywords: + - HasEventBindings + - getEventBindings + - event transformer + - platform integration +related: + - introduction + - has-listeners + - action-binding-strategy +see_also: + - ../introduction + - ../../../core-concepts/bootstrapping/initializers/event-binding +noindex: false +--- + +# HasEventBindings Interface + +The `HasEventBindings` interface provides flexible event binding configuration, primarily used for bridging platform-specific events to your application's event system. + +--- + +## Interface Definition + +```php +namespace PHPNomad\Events\Interfaces; + +interface HasEventBindings +{ + /** + * @return array + */ + public function getEventBindings(): array; +} +``` + +--- + +## Method Reference + +### `getEventBindings(): array` + +Returns an array of event binding configurations. + +| Aspect | Details | +|--------|---------| +| Returns | `array` — Binding configurations | + +**Return format:** +```php +[ + EventClass::class => [ + [ + 'action' => 'platform_action_name', + 'transformer' => callable, + ], + ], +] +``` + +--- + +## HasEventBindings vs HasListeners + +| Interface | Purpose | Use When | +|-----------|---------|----------| +| `HasListeners` | Map events to handlers | Responding to internal events | +| `HasEventBindings` | Bridge external events | Connecting platform events to your app | + +``` +HasListeners: +Internal Event → Your Handler + +HasEventBindings: +Platform Action → Transformer → Your Event → Your Handlers +``` + +--- + +## Basic Usage + +### Simple Binding + +```php +use PHPNomad\Events\Interfaces\HasEventBindings; + +class WordPressIntegration implements HasEventBindings +{ + public function getEventBindings(): array + { + return [ + UserCreatedEvent::class => [ + [ + 'action' => 'user_register', + 'transformer' => function($userId) { + $user = get_userdata($userId); + return new UserCreatedEvent( + userId: $userId, + email: $user->user_email, + createdAt: new \DateTimeImmutable() + ); + }, + ], + ], + ]; + } +} +``` + +### Multiple Actions for One Event + +Different platform actions can trigger the same application event: + +```php +public function getEventBindings(): array +{ + return [ + OrderCreatedEvent::class => [ + // WooCommerce new order + [ + 'action' => 'woocommerce_new_order', + 'transformer' => [$this, 'fromWooCommerce'], + ], + // Easy Digital Downloads purchase + [ + 'action' => 'edd_complete_purchase', + 'transformer' => [$this, 'fromEdd'], + ], + ], + ]; +} + +private function fromWooCommerce($orderId): OrderCreatedEvent +{ + $order = wc_get_order($orderId); + return new OrderCreatedEvent( + orderId: $orderId, + customerId: $order->get_customer_id(), + total: $order->get_total() + ); +} + +private function fromEdd($paymentId): OrderCreatedEvent +{ + $payment = new EDD_Payment($paymentId); + return new OrderCreatedEvent( + orderId: $paymentId, + customerId: $payment->customer_id, + total: $payment->total + ); +} +``` + +--- + +## Transformers + +Transformers convert platform-specific data into your application events. + +### Closure Transformer + +```php +'transformer' => function($postId, $post) { + return new PostPublishedEvent( + postId: $postId, + title: $post->post_title, + authorId: $post->post_author + ); +} +``` + +### Method Transformer + +```php +'transformer' => [$this, 'transformPost'] + +// ... + +private function transformPost($postId, $post): PostPublishedEvent +{ + return new PostPublishedEvent( + postId: $postId, + title: $post->post_title, + authorId: $post->post_author + ); +} +``` + +### Service Transformer + +```php +'transformer' => [$this->postTransformer, 'toEvent'] +``` + +### Conditional Transformation + +Return `null` to skip event creation: + +```php +'transformer' => function($postId, $post, $update) { + // Only trigger for new posts, not updates + if ($update) { + return null; + } + + // Only trigger for published posts + if ($post->post_status !== 'publish') { + return null; + } + + return new PostPublishedEvent($postId); +} +``` + +--- + +## Real-World Example + +### WordPress + WooCommerce Integration + +```php +class WooCommerceEventBindings implements HasEventBindings +{ + public function __construct( + private OrderTransformer $orderTransformer, + private CustomerTransformer $customerTransformer + ) {} + + public function getEventBindings(): array + { + return [ + // Order lifecycle events + OrderPlacedEvent::class => [ + [ + 'action' => 'woocommerce_checkout_order_processed', + 'transformer' => [$this->orderTransformer, 'toOrderPlaced'], + ], + ], + + OrderCompletedEvent::class => [ + [ + 'action' => 'woocommerce_order_status_completed', + 'transformer' => [$this->orderTransformer, 'toOrderCompleted'], + ], + ], + + OrderCancelledEvent::class => [ + [ + 'action' => 'woocommerce_order_status_cancelled', + 'transformer' => [$this->orderTransformer, 'toOrderCancelled'], + ], + ], + + // Customer events + CustomerCreatedEvent::class => [ + [ + 'action' => 'woocommerce_created_customer', + 'transformer' => [$this->customerTransformer, 'toCustomerCreated'], + ], + ], + + // Product events + ProductPurchasedEvent::class => [ + [ + 'action' => 'woocommerce_order_item_added_to_order', + 'transformer' => function($itemId, $item, $orderId) { + return new ProductPurchasedEvent( + productId: $item->get_product_id(), + orderId: $orderId, + quantity: $item->get_quantity() + ); + }, + ], + ], + ]; + } +} +``` + +--- + +## Best Practices + +### 1. Use Service Classes for Complex Transformations + +```php +// Good: dedicated transformer service +class OrderTransformer +{ + public function toOrderPlaced($orderId): OrderPlacedEvent + { + $order = wc_get_order($orderId); + return new OrderPlacedEvent( + orderId: $orderId, + customerId: $order->get_customer_id(), + total: (float) $order->get_total(), + items: $this->extractItems($order), + placedAt: new \DateTimeImmutable($order->get_date_created()) + ); + } + + private function extractItems($order): array + { + // Complex item extraction logic + } +} +``` + +### 2. Handle Missing Data Gracefully + +```php +'transformer' => function($postId) { + $post = get_post($postId); + + if (!$post) { + return null; // Skip if post doesn't exist + } + + return new PostPublishedEvent($postId); +} +``` + +### 3. Document Platform Actions + +```php +public function getEventBindings(): array +{ + return [ + ReportCreatedEvent::class => [ + [ + // WordPress 'save_post' action + // @param int $post_id + // @param WP_Post $post + // @param bool $update - true if updating existing post + 'action' => 'save_post', + 'transformer' => function($postId, $post, $update) { + if ($update || $post->post_type !== 'report') { + return null; + } + return new ReportCreatedEvent($postId); + }, + ], + ], + ]; +} +``` + +--- + +## Testing + +```php +class WooCommerceEventBindingsTest extends TestCase +{ + public function test_binds_order_completed_event(): void + { + $bindings = new WooCommerceEventBindings( + new OrderTransformer(), + new CustomerTransformer() + ); + + $eventBindings = $bindings->getEventBindings(); + + $this->assertArrayHasKey(OrderCompletedEvent::class, $eventBindings); + $this->assertEquals( + 'woocommerce_order_status_completed', + $eventBindings[OrderCompletedEvent::class][0]['action'] + ); + } +} +``` + +--- + +## Related Interfaces + +- **[HasListeners](has-listeners)** — For internal event handling +- **[ActionBindingStrategy](action-binding-strategy)** — Executes the bindings +- **[Event](event)** — Events created by transformers + +--- + +## Further Reading + +- **[Event Bindings Guide](/core-concepts/bootstrapping/initializers/event-binding)** — Tutorial-style guide with WordPress examples +- **[Best Practices](../patterns/best-practices)** — Transformer patterns and testing strategies diff --git a/public/docs/packages/event/interfaces/has-listeners.md b/public/docs/packages/event/interfaces/has-listeners.md new file mode 100644 index 0000000..62e41fa --- /dev/null +++ b/public/docs/packages/event/interfaces/has-listeners.md @@ -0,0 +1,373 @@ +--- +id: event-interface-has-listeners +slug: docs/packages/event/interfaces/has-listeners +title: HasListeners Interface +doc_type: reference +status: active +language: en +owner: docs-team +last_reviewed: 2026-01-25 +applies_to: ["all"] +canonical: true +summary: The HasListeners interface declares which events a module listens to and their handlers. +llm_summary: > + HasListeners enables declarative event subscription in phpnomad/event. Classes implement + getListeners() to return an array mapping event class names to handler class names. Supports + single handlers or arrays of handlers per event. The loader/bootstrapper reads these mappings + and registers them with EventStrategy. Typically used in initializer classes to declare a + module's event subscriptions. Handlers are resolved through the DI container when events fire. +questions_answered: + - What is HasListeners? + - How do I declare event listeners? + - Can I have multiple handlers for one event? + - How does HasListeners work with the DI container? + - When should I use HasListeners vs attach()? +audience: + - developers + - backend engineers +tags: + - events + - interface + - reference + - configuration +llm_tags: + - has-listeners + - event-subscription + - declarative-config +keywords: + - HasListeners + - getListeners + - event subscription +related: + - introduction + - can-handle + - has-event-bindings +see_also: + - ../introduction + - ../../../core-concepts/bootstrapping/initializers/event-listeners +noindex: false +--- + +# HasListeners Interface + +The `HasListeners` interface provides declarative event subscription. Instead of manually calling `attach()`, you declare which events your module listens to and which handlers respond. + +--- + +## Interface Definition + +```php +namespace PHPNomad\Events\Interfaces; + +interface HasListeners +{ + /** + * Gets the listeners and their handlers. + * + * @return array, class-string[]|class-string> + */ + public function getListeners(): array; +} +``` + +--- + +## Method Reference + +### `getListeners(): array` + +Returns a mapping of event classes to handler classes. + +| Aspect | Details | +|--------|---------| +| Returns | `array` — Event class names mapped to handler class names | + +**Return format:** +```php +[ + EventClass::class => HandlerClass::class, + // or + EventClass::class => [HandlerClass1::class, HandlerClass2::class], +] +``` + +--- + +## Basic Usage + +### Single Handler per Event + +```php +use PHPNomad\Events\Interfaces\HasListeners; + +class UserModule implements HasListeners +{ + public function getListeners(): array + { + return [ + UserCreatedEvent::class => SendWelcomeEmailHandler::class, + UserDeletedEvent::class => CleanupUserDataHandler::class, + ]; + } +} +``` + +### Multiple Handlers per Event + +```php +class OrderModule implements HasListeners +{ + public function getListeners(): array + { + return [ + OrderPlacedEvent::class => [ + SendOrderConfirmationHandler::class, + UpdateInventoryHandler::class, + NotifyWarehouseHandler::class, + ], + ]; + } +} +``` + +### Mixed Single and Multiple + +```php +class NotificationModule implements HasListeners +{ + public function getListeners(): array + { + return [ + // Single handler + UserLoggedInEvent::class => UpdateLastLoginHandler::class, + + // Multiple handlers + PaymentReceivedEvent::class => [ + SendReceiptHandler::class, + UpdateAccountBalanceHandler::class, + NotifyAccountingHandler::class, + ], + ]; + } +} +``` + +--- + +## How It Works + +The bootstrapper/loader reads `HasListeners` implementations and registers handlers: + +``` +┌─────────────────────┐ +│ Your Module │ +│ implements │ +│ HasListeners │ +└──────────┬──────────┘ + │ getListeners() + ▼ +┌─────────────────────┐ +│ Bootstrapper/ │ +│ Loader │ +└──────────┬──────────┘ + │ for each event → handler + ▼ +┌─────────────────────┐ +│ EventStrategy │ +│ attach(event, │ +│ container->get( │ +│ handler)) │ +└─────────────────────┘ +``` + +When an event fires: +1. `EventStrategy` calls the registered listener +2. The listener resolves the handler through the DI container +3. The handler's `handle()` method is called + +--- + +## Where to Use HasListeners + +### In Initializers + +The most common location is in initializer classes: + +```php +class ApplicationInitializer implements HasListeners +{ + public function getListeners(): array + { + return [ + UserCreatedEvent::class => [ + CreateUserProfileHandler::class, + SendWelcomeEmailHandler::class, + ], + ]; + } +} +``` + +### In Service Providers + +```php +class PaymentServiceProvider implements HasListeners +{ + public function getListeners(): array + { + return [ + PaymentReceivedEvent::class => ProcessPaymentHandler::class, + PaymentFailedEvent::class => HandlePaymentFailureHandler::class, + RefundRequestedEvent::class => ProcessRefundHandler::class, + ]; + } +} +``` + +### In Module Classes + +```php +class BlogModule implements HasListeners +{ + public function getListeners(): array + { + return [ + PostPublishedEvent::class => [ + NotifySubscribersHandler::class, + UpdateSearchIndexHandler::class, + InvalidateCacheHandler::class, + ], + ]; + } +} +``` + +--- + +## HasListeners vs Direct attach() + +| Approach | When to Use | +|----------|-------------| +| `HasListeners` | Standard case—declaring module's event subscriptions | +| Direct `attach()` | Dynamic listeners, conditional registration, closures | + +### HasListeners (Declarative) + +```php +// Clean, discoverable, testable +class MyModule implements HasListeners +{ + public function getListeners(): array + { + return [ + SomeEvent::class => SomeHandler::class, + ]; + } +} +``` + +### Direct attach() (Imperative) + +```php +// For dynamic or conditional listeners +$events->attach('some.event', function($event) use ($config) { + if ($config->isFeatureEnabled('notifications')) { + // handle + } +}); +``` + +--- + +## Best Practices + +### 1. Group Related Listeners + +Organize listeners by domain: + +```php +// Good: cohesive module +class InventoryModule implements HasListeners +{ + public function getListeners(): array + { + return [ + OrderPlacedEvent::class => ReserveInventoryHandler::class, + OrderCancelledEvent::class => ReleaseInventoryHandler::class, + ShipmentDispatchedEvent::class => DeductInventoryHandler::class, + ]; + } +} +``` + +### 2. Use Descriptive Handler Names + +Handler names should indicate what they do: + +```php +// Good: clear purpose +SendOrderConfirmationEmailHandler::class +UpdateCustomerLoyaltyPointsHandler::class +SyncInventoryWithWarehouseHandler::class + +// Bad: vague names +OrderHandler::class +ProcessHandler::class +HandleEvent::class +``` + +### 3. Order Handlers by Importance + +List critical handlers first: + +```php +public function getListeners(): array +{ + return [ + PaymentReceivedEvent::class => [ + ValidatePaymentHandler::class, // Critical: must run first + UpdateOrderStatusHandler::class, // Important: business logic + SendReceiptEmailHandler::class, // Nice-to-have: notification + UpdateAnalyticsHandler::class, // Optional: analytics + ], + ]; +} +``` + +--- + +## Testing + +Test that your module declares the expected listeners: + +```php +class OrderModuleTest extends TestCase +{ + public function test_declares_order_listeners(): void + { + $module = new OrderModule(); + $listeners = $module->getListeners(); + + $this->assertArrayHasKey(OrderPlacedEvent::class, $listeners); + $this->assertContains( + SendOrderConfirmationHandler::class, + $listeners[OrderPlacedEvent::class] + ); + } +} +``` + +--- + +## Related Interfaces + +- **[CanHandle](can-handle)** — The handlers that are registered +- **[Event](event)** — The events being listened for +- **[HasEventBindings](has-event-bindings)** — Alternative with more flexibility + +--- + +## Further Reading + +- **[Event Listeners Guide](/core-concepts/bootstrapping/initializers/event-listeners)** — Tutorial-style guide with examples +- **[Best Practices](../patterns/best-practices)** — Handler patterns and testing strategies diff --git a/public/docs/packages/event/interfaces/introduction.md b/public/docs/packages/event/interfaces/introduction.md new file mode 100644 index 0000000..e271806 --- /dev/null +++ b/public/docs/packages/event/interfaces/introduction.md @@ -0,0 +1,178 @@ +--- +id: event-interfaces-introduction +slug: docs/packages/event/interfaces/introduction +title: Event Interfaces Overview +doc_type: reference +status: active +language: en +owner: docs-team +last_reviewed: 2026-01-25 +applies_to: ["all"] +canonical: true +summary: Overview of all interfaces in the event package for implementing event-driven architecture. +llm_summary: > + The phpnomad/event package provides six interfaces for event-driven architecture: Event (identifiable + events), EventStrategy (broadcasting and listener management), CanHandle (event handlers), HasListeners + (declaring event subscriptions), HasEventBindings (flexible binding configuration), and ActionBindingStrategy + (bridging external systems to internal events). These interfaces enable decoupled, testable architectures + where components communicate through events rather than direct calls. +questions_answered: + - What interfaces are in the event package? + - How do the event interfaces relate to each other? + - Which interface should I implement for my use case? +audience: + - developers + - backend engineers +tags: + - events + - interfaces + - reference +llm_tags: + - event-interfaces + - api-reference +keywords: + - event interfaces + - EventStrategy + - CanHandle + - HasListeners +related: + - ../introduction +see_also: + - event + - event-strategy + - can-handle + - has-listeners + - has-event-bindings + - action-binding-strategy +noindex: false +--- + +# Event Interfaces + +The `phpnomad/event` package provides six interfaces that work together to enable event-driven architecture. Each interface has a specific role in the event system. + +--- + +## Interface Overview + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Event System │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────┐ broadcasts ┌───────────────┐ │ +│ │ Event │ ──────────────────▶│ EventStrategy │ │ +│ └─────────┘ └───────┬───────┘ │ +│ ▲ │ │ +│ │ implements │ calls │ +│ │ ▼ │ +│ ┌─────────────┐ ┌───────────────┐ │ +│ │ Your Event │ │ CanHandle │ │ +│ │ Classes │ │ (handlers) │ │ +│ └─────────────┘ └───────────────┘ │ +│ ▲ │ +│ │ registered by │ +│ ┌──────────────┴──────────────┐ │ +│ │ │ │ +│ ┌───────────────┐ ┌──────────────────┐│ +│ │ HasListeners │ │ HasEventBindings ││ +│ │ (declarative) │ │ (flexible) ││ +│ └───────────────┘ └──────────────────┘│ +│ │ +│ ┌───────────────────────┐ │ +│ │ ActionBindingStrategy │ ← Bridges external systems │ +│ └───────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Quick Reference + +| Interface | Purpose | Key Method | +|-----------|---------|------------| +| [Event](event) | Identify events | `getId(): string` | +| [EventStrategy](event-strategy) | Broadcast and manage listeners | `broadcast()`, `attach()`, `detach()` | +| [CanHandle](can-handle) | Handle events | `handle(Event): void` | +| [HasListeners](has-listeners) | Declare event subscriptions | `getListeners(): array` | +| [HasEventBindings](has-event-bindings) | Flexible binding configuration | `getEventBindings(): array` | +| [ActionBindingStrategy](action-binding-strategy) | Bridge external systems | `bindAction()` | + +--- + +## Choosing the Right Interface + +### You want to create an event class +Implement **[Event](event)**. Your class represents something that happened. + +```php +class UserCreatedEvent implements Event +{ + public static function getId(): string + { + return 'user.created'; + } +} +``` + +### You want to handle events +Implement **[CanHandle](can-handle)**. Your class responds when specific events occur. + +```php +class SendWelcomeEmailHandler implements CanHandle +{ + public function handle(Event $event): void + { + // React to the event + } +} +``` + +### You want to broadcast events +Inject **[EventStrategy](event-strategy)**. Use it to dispatch events to all listeners. + +```php +class UserService +{ + public function __construct(private EventStrategy $events) {} + + public function createUser(): void + { + // ... create user + $this->events->broadcast(new UserCreatedEvent($user)); + } +} +``` + +### You want to declare what events a module listens to +Implement **[HasListeners](has-listeners)**. Map event classes to handler classes. + +```php +class MyModule implements HasListeners +{ + public function getListeners(): array + { + return [ + UserCreatedEvent::class => SendWelcomeEmailHandler::class, + ]; + } +} +``` + +### You want flexible event binding configuration +Implement **[HasEventBindings](has-event-bindings)**. Provides more configuration options than HasListeners. + +### You want to bridge platform events to your application +Use **[ActionBindingStrategy](action-binding-strategy)**. Connects external systems (like WordPress hooks) to your internal events. + +--- + +## Interface Details + +- **[Event](event)** — Base interface all events must implement +- **[EventStrategy](event-strategy)** — Core dispatcher for broadcasting and listener management +- **[CanHandle](can-handle)** — Handler interface for responding to events +- **[HasListeners](has-listeners)** — Declarative event-to-handler mapping +- **[HasEventBindings](has-event-bindings)** — Flexible event binding configuration +- **[ActionBindingStrategy](action-binding-strategy)** — Bridge to external event systems diff --git a/public/docs/packages/event/introduction.md b/public/docs/packages/event/introduction.md new file mode 100644 index 0000000..f72cc67 --- /dev/null +++ b/public/docs/packages/event/introduction.md @@ -0,0 +1,263 @@ +--- +id: event-introduction +slug: docs/packages/event/introduction +title: Event Package +doc_type: explanation +status: active +language: en +owner: docs-team +last_reviewed: 2026-01-25 +applies_to: ["all"] +canonical: true +summary: The event package provides interfaces for implementing event-driven architecture with broadcasting, listening, and handler patterns. +llm_summary: > + phpnomad/event provides a set of interfaces for implementing event-driven architecture in PHP. + The package defines Event (identifiable events), EventStrategy (broadcast/attach/detach operations), + CanHandle (event handlers), HasListeners (objects that provide listener mappings), HasEventBindings + (objects that provide event bindings), and ActionBindingStrategy (binding actions to events). + Zero dependencies. Used by auth, rest, update, core, database, wordpress-integration and many other + packages. Implementations include symfony-event-dispatcher-integration for Symfony EventDispatcher. +questions_answered: + - What is the event package? + - How do I implement event-driven architecture in PHPNomad? + - How do I broadcast events? + - How do I listen for events? + - What is an EventStrategy? + - How do I create event handlers? + - What packages use the event system? +audience: + - developers + - backend engineers + - architects +tags: + - events + - event-driven + - design-pattern + - pub-sub +llm_tags: + - event-pattern + - publish-subscribe + - observer-pattern + - event-broadcasting +keywords: + - phpnomad event + - event driven php + - EventStrategy + - event broadcasting + - event listeners +related: + - ../di/introduction + - ../database/introduction + - ../database/caching-and-events + - ../../core-concepts/bootstrapping/initializers/event-listeners + - ../../core-concepts/bootstrapping/initializers/event-binding +see_also: + - interfaces/introduction + - patterns/best-practices + - ../symfony-event-dispatcher-integration/introduction +noindex: false +--- + +# Event + +`phpnomad/event` provides **interfaces for event-driven architecture** in PHP applications. Instead of tightly coupling components, the event system lets you: + +* **Decouple publishers from subscribers** — Components communicate through events, not direct calls +* **React to state changes** — Listen for events like "record created" or "user logged in" +* **Extend behavior** — Add functionality without modifying existing code +* **Chain actions** — One event can trigger multiple handlers in sequence + +--- + +## Key Concepts + +| Concept | Description | +|---------|-------------| +| [Event](interfaces/event) | An object representing something that happened | +| [EventStrategy](interfaces/event-strategy) | The dispatcher that broadcasts events and manages listeners | +| [CanHandle](interfaces/can-handle) | Handler classes that respond to specific events | +| [HasListeners](interfaces/has-listeners) | Declares which events a module listens to | +| [HasEventBindings](interfaces/has-event-bindings) | Flexible binding configuration for platform integration | +| [ActionBindingStrategy](interfaces/action-binding-strategy) | Bridges external systems to internal events | + +See [Interfaces Overview](interfaces/introduction) for detailed documentation of each interface. + +--- + +## The Event Lifecycle + +``` +Something happens in your application + │ + ▼ +┌───────────────────────────┐ +│ Create Event object │ +│ implements Event │ +└───────────────────────────┘ + │ + ▼ +┌───────────────────────────┐ +│ EventStrategy │ +│ broadcast($event) │ +└───────────────────────────┘ + │ + ▼ +┌───────────────────────────┐ +│ Handlers are called │ +│ in priority order │ +└───────────────────────────┘ + │ + +────┼────+────+ + │ │ │ │ + ▼ ▼ ▼ ▼ + Send Update Log Notify + Email Cache Event Slack +``` + +--- + +## Installation + +```bash +composer require phpnomad/event +``` + +**Requirements:** PHP 7.4+ + +**Dependencies:** None (zero dependencies) + +This package provides **interfaces only**. For a working implementation, install an integration: + +```bash +composer require phpnomad/symfony-event-dispatcher-integration +``` + +--- + +## Quick Example + +### Creating an Event + +```php +use PHPNomad\Events\Interfaces\Event; + +class UserCreatedEvent implements Event +{ + public function __construct( + public readonly int $userId, + public readonly string $email + ) {} + + public static function getId(): string + { + return 'user.created'; + } +} +``` + +### Creating a Handler + +```php +use PHPNomad\Events\Interfaces\CanHandle; +use PHPNomad\Events\Interfaces\Event; + +class SendWelcomeEmailHandler implements CanHandle +{ + public function __construct(private EmailService $email) {} + + public function handle(Event $event): void + { + $this->email->send($event->email, 'Welcome!'); + } +} +``` + +### Declaring Listeners + +```php +use PHPNomad\Events\Interfaces\HasListeners; + +class UserModule implements HasListeners +{ + public function getListeners(): array + { + return [ + UserCreatedEvent::class => SendWelcomeEmailHandler::class, + ]; + } +} +``` + +### Broadcasting Events + +```php +class UserService +{ + public function __construct(private EventStrategy $events) {} + + public function createUser(string $email): User + { + $user = new User($email); + // save user... + + $this->events->broadcast(new UserCreatedEvent( + userId: $user->getId(), + email: $user->getEmail() + )); + + return $user; + } +} +``` + +--- + +## When to Use Events + +| Scenario | Why Events Help | +|----------|-----------------| +| Multiple reactions | One action triggers email, logging, cache update | +| Decoupled modules | Modules communicate without direct dependencies | +| Extension points | Add behavior without modifying existing code | +| Audit trails | Log all significant actions centrally | + +See [Best Practices](patterns/best-practices) for detailed guidance on when to use events and when not to. + +--- + +## Packages That Use Events + +| Package | How It Uses Events | +|---------|-------------------| +| [database](../database/introduction) | Broadcasts RecordCreated, RecordUpdated, RecordDeleted | +| [auth](../auth/introduction) | Authentication lifecycle events | +| [rest](../rest/introduction) | Request/response events via [EventInterceptor](../rest/interceptors/included-interceptors/event-interceptor) | +| [update](../update/introduction) | Update lifecycle events | +| [wordpress-integration](../wordpress-integration/introduction) | Bridges WordPress hooks to application events | + +--- + +## Further Reading + +### Package Documentation + +* [Interfaces Overview](interfaces/introduction) — All six interfaces explained +* [Best Practices](patterns/best-practices) — Event design, handler patterns, testing strategies + +### Related Core Concepts + +* [Event Listeners](/core-concepts/bootstrapping/initializers/event-listeners) — Setting up listeners in initializers +* [Event Bindings](/core-concepts/bootstrapping/initializers/event-binding) — Bridging platform events to application events +* [Caching and Events](/packages/database/caching-and-events) — Database CRUD events + +### Implementations + +* [Symfony Event Dispatcher Integration](../symfony-event-dispatcher-integration/introduction) — Production-ready EventStrategy implementation + +--- + +## Next Steps + +1. **Learn the interfaces** → [Interfaces Overview](interfaces/introduction) +2. **See best practices** → [Best Practices](patterns/best-practices) +3. **Get an implementation** → [Symfony Integration](../symfony-event-dispatcher-integration/introduction) diff --git a/public/docs/packages/event/patterns/best-practices.md b/public/docs/packages/event/patterns/best-practices.md new file mode 100644 index 0000000..f8c5a13 --- /dev/null +++ b/public/docs/packages/event/patterns/best-practices.md @@ -0,0 +1,621 @@ +--- +id: event-patterns-best-practices +slug: docs/packages/event/patterns/best-practices +title: Event System Best Practices +doc_type: how-to +status: active +language: en +owner: docs-team +last_reviewed: 2026-01-25 +applies_to: ["all"] +canonical: true +summary: Best practices, common patterns, and testing strategies for PHPNomad's event system. +llm_summary: > + Comprehensive guide to event-driven architecture best practices in PHPNomad. Covers event design + (immutability, past tense naming, sufficient context), handler patterns (single responsibility, + dependency injection, error handling), testing strategies (mocking EventStrategy, testing handlers + in isolation), and common anti-patterns to avoid. Includes real-world e-commerce example with + OrderPlacedEvent, PaymentReceivedEvent, and associated handlers demonstrating proper structure. +questions_answered: + - What are best practices for events? + - How should I design event classes? + - How do I test event handlers? + - What are common event anti-patterns? + - How do I test that events are broadcast? + - When should I use events vs direct calls? +audience: + - developers + - backend engineers +tags: + - events + - best-practices + - patterns + - testing +llm_tags: + - event-best-practices + - event-testing + - event-patterns +keywords: + - event best practices + - event testing + - event patterns + - event anti-patterns +related: + - ../introduction + - ../interfaces/introduction +see_also: + - ../interfaces/event + - ../interfaces/can-handle +noindex: false +--- + +# Event System Best Practices + +This guide covers best practices for designing events, writing handlers, and testing event-driven code in PHPNomad. + +--- + +## Event Design + +### 1. Use Past Tense Names + +Events represent things that **have happened**: + +```php +// Good: past tense +class UserCreatedEvent {} +class OrderPlacedEvent {} +class PaymentReceivedEvent {} +class ShipmentDispatchedEvent {} + +// Bad: present/imperative (sounds like commands) +class CreateUserEvent {} +class PlaceOrderEvent {} +class ReceivePaymentEvent {} +``` + +### 2. Make Events Immutable + +Events are historical records—they shouldn't change after creation: + +```php +// Good: immutable with readonly properties (PHP 8.1+) +class OrderPlacedEvent implements Event +{ + public function __construct( + public readonly int $orderId, + public readonly int $customerId, + public readonly float $total, + public readonly \DateTimeImmutable $placedAt + ) {} +} + +// Bad: mutable properties +class OrderPlacedEvent implements Event +{ + public int $orderId; // Can be modified! + public float $total; // Handlers could change this! +} +``` + +### 3. Include Sufficient Context + +Events should carry all data handlers need—avoid forcing handlers to query for more: + +```php +// Good: complete context +class InvoiceSentEvent implements Event +{ + public function __construct( + public readonly int $invoiceId, + public readonly int $customerId, + public readonly string $customerEmail, + public readonly float $amount, + public readonly \DateTimeImmutable $sentAt + ) {} +} + +// Bad: insufficient context +class InvoiceSentEvent implements Event +{ + public function __construct( + public readonly int $invoiceId // Handlers must query for customer, amount, etc. + ) {} +} +``` + +### 4. Use Value Objects for Complex Data + +```php +class ShipmentDispatchedEvent implements Event +{ + public function __construct( + public readonly int $orderId, + public readonly Address $destination, // Value object + public readonly Carrier $carrier, // Value object + public readonly TrackingInfo $tracking, // Value object + public readonly \DateTimeImmutable $dispatchedAt + ) {} +} +``` + +--- + +## Handler Design + +### 1. Single Responsibility + +Each handler should do **one thing**: + +```php +// Good: focused handlers +class SendWelcomeEmailHandler implements CanHandle { /* sends email */ } +class CreateUserProfileHandler implements CanHandle { /* creates profile */ } +class AddToMailingListHandler implements CanHandle { /* adds to list */ } +class LogUserCreationHandler implements CanHandle { /* logs event */ } + +// Bad: handler does too much +class HandleUserCreatedHandler implements CanHandle +{ + public function handle(Event $event): void + { + $this->sendWelcomeEmail($event); + $this->createProfile($event); + $this->addToMailingList($event); + $this->logCreation($event); + $this->notifyAdmin($event); + } +} +``` + +### 2. Inject Dependencies + +Let the container provide what handlers need: + +```php +// Good: dependencies injected +class SendWelcomeEmailHandler implements CanHandle +{ + public function __construct( + private EmailStrategy $email, + private TemplateRenderer $templates, + private LoggerStrategy $logger + ) {} + + public function handle(Event $event): void + { + // Use injected dependencies + } +} + +// Bad: creating dependencies +class SendWelcomeEmailHandler implements CanHandle +{ + public function handle(Event $event): void + { + $email = new SmtpEmailService(/* config */); // Hard to test! + $email->send(/* ... */); + } +} +``` + +### 3. Handle Errors Gracefully + +Don't let one handler break others: + +```php +class NotifySlackHandler implements CanHandle +{ + public function __construct( + private SlackClient $slack, + private LoggerStrategy $logger + ) {} + + public function handle(Event $event): void + { + try { + $this->slack->notify($this->formatMessage($event)); + } catch (SlackException $e) { + // Log but don't re-throw—let other handlers run + $this->logger->warning('Slack notification failed', [ + 'event' => $event->getId(), + 'error' => $e->getMessage(), + ]); + } + } +} +``` + +### 4. Keep Handlers Fast + +Queue long-running operations: + +```php +// Good: queue heavy work +class GenerateInvoicePdfHandler implements CanHandle +{ + public function __construct(private Queue $queue) {} + + public function handle(Event $event): void + { + $this->queue->push(new GenerateInvoicePdfJob( + $event->orderId + )); + } +} + +// Bad: blocks event dispatch +class GenerateInvoicePdfHandler implements CanHandle +{ + public function handle(Event $event): void + { + $pdf = $this->generatePdf($event->orderId); // Takes 30 seconds! + $this->saveToDisk($pdf); + $this->uploadToS3($pdf); + } +} +``` + +--- + +## When to Use Events + +Events are appropriate when: + +| Scenario | Why Events | +|----------|------------| +| Multiple reactions | One action triggers email, logging, analytics | +| Decoupled modules | Modules communicate without direct dependencies | +| Extension points | Allow adding behavior without modifying code | +| Audit/compliance | Log all significant actions | +| Eventual consistency | Components update asynchronously | + +### Good Use Cases + +```php +// User lifecycle +$events->broadcast(new UserRegisteredEvent($user)); +$events->broadcast(new UserVerifiedEvent($user)); +$events->broadcast(new UserDeletedEvent($userId)); + +// Business events +$events->broadcast(new OrderPlacedEvent($order)); +$events->broadcast(new PaymentReceivedEvent($payment)); +$events->broadcast(new ShipmentDispatchedEvent($shipment)); +``` + +--- + +## When NOT to Use Events + +### Need a Return Value + +```php +// Bad: events don't return values +$event = new ValidateOrderEvent($order); +$events->broadcast($event); +$isValid = $event->isValid; // Awkward! + +// Good: direct call +$isValid = $this->validator->validate($order); +``` + +### Simple 1:1 Relationships + +```php +// Overkill: event for simple call +$events->broadcast(new CalculateTaxEvent($price)); + +// Just call the service +$tax = $this->taxCalculator->calculate($price); +``` + +### Performance-Critical Code + +```php +// Bad: event overhead in tight loop +foreach ($items as $item) { + $events->broadcast(new ItemProcessedEvent($item)); +} + +// Better: batch event +$events->broadcast(new ItemsProcessedEvent($items)); + +// Or: no event if just internal processing +foreach ($items as $item) { + $this->process($item); +} +``` + +--- + +## Testing Strategies + +### Testing Event Broadcasting + +Verify that services broadcast the right events: + +```php +class OrderServiceTest extends TestCase +{ + public function test_broadcasts_order_placed_event(): void + { + $events = $this->createMock(EventStrategy::class); + + $events->expects($this->once()) + ->method('broadcast') + ->with($this->callback(function(Event $event) { + return $event instanceof OrderPlacedEvent + && $event->customerId === 123 + && $event->total === 99.99; + })); + + $service = new OrderService($events, $this->orders); + $service->placeOrder($this->cart, $this->customer); + } +} +``` + +### Testing Handlers in Isolation + +```php +class SendOrderConfirmationHandlerTest extends TestCase +{ + public function test_sends_confirmation_email(): void + { + $email = $this->createMock(EmailStrategy::class); + + $email->expects($this->once()) + ->method('send') + ->with( + 'customer@example.com', + 'Order Confirmation', + $this->stringContains('Order #456') + ); + + $handler = new SendOrderConfirmationHandler($email); + $handler->handle(new OrderPlacedEvent( + orderId: 456, + customerId: 123, + customerEmail: 'customer@example.com', + total: 99.99, + placedAt: new \DateTimeImmutable() + )); + } +} +``` + +### Capturing Events for Assertions + +```php +class SpyEventStrategy implements EventStrategy +{ + public array $events = []; + + public function broadcast(Event $event): void + { + $this->events[] = $event; + } + + public function attach(string $event, callable $action, ?int $priority = null): void {} + public function detach(string $event, callable $action, ?int $priority = null): void {} +} + +// In test +public function test_order_flow_broadcasts_expected_events(): void +{ + $events = new SpyEventStrategy(); + $service = new OrderService($events, $this->orders, $this->payments); + + $service->placeOrder($this->cart); + $service->processPayment($this->order, $this->paymentMethod); + + $this->assertCount(2, $events->events); + $this->assertInstanceOf(OrderPlacedEvent::class, $events->events[0]); + $this->assertInstanceOf(PaymentReceivedEvent::class, $events->events[1]); +} +``` + +### Testing HasListeners Declarations + +```php +class OrderModuleTest extends TestCase +{ + public function test_registers_expected_listeners(): void + { + $module = new OrderModule(); + $listeners = $module->getListeners(); + + $this->assertArrayHasKey(OrderPlacedEvent::class, $listeners); + $this->assertContains( + SendOrderConfirmationHandler::class, + (array) $listeners[OrderPlacedEvent::class] + ); + } +} +``` + +--- + +## Common Anti-Patterns + +### Mutable Events + +```php +// Anti-pattern: event modified by handlers +class UserUpdatedEvent implements Event +{ + public array $changes = []; +} + +// Handler 1 adds to changes +// Handler 2 reads changes but sees Handler 1's modifications +// Order-dependent, hard to debug +``` + +### Event Chains + +```php +// Anti-pattern: handler broadcasts another event +class UpdateInventoryHandler implements CanHandle +{ + public function handle(Event $event): void + { + $this->inventory->reduce($event->items); + $this->events->broadcast(new InventoryUpdatedEvent()); // Cascades! + } +} + +// Can lead to infinite loops or hard-to-trace flows +``` + +### Using Events for Control Flow + +```php +// Anti-pattern: using events to control execution +class ProcessOrderHandler implements CanHandle +{ + public function handle(Event $event): void + { + if (!$event->validated) { // Checking state set by another handler + return; + } + // process... + } +} +``` + +### Over-Eventing + +```php +// Anti-pattern: event for every tiny thing +$events->broadcast(new DatabaseQueryExecutedEvent()); +$events->broadcast(new CacheHitEvent()); +$events->broadcast(new LogMessageWrittenEvent()); + +// Creates noise, performance overhead +``` + +--- + +## Real-World Example + +### E-commerce Order System + +```php +// Events +class OrderPlacedEvent implements Event +{ + public function __construct( + public readonly int $orderId, + public readonly int $customerId, + public readonly string $customerEmail, + public readonly float $total, + public readonly array $items, + public readonly \DateTimeImmutable $placedAt + ) {} + + public static function getId(): string { return 'order.placed'; } +} + +class PaymentReceivedEvent implements Event +{ + public function __construct( + public readonly int $orderId, + public readonly string $transactionId, + public readonly float $amount, + public readonly \DateTimeImmutable $receivedAt + ) {} + + public static function getId(): string { return 'payment.received'; } +} + +// Handlers +class SendOrderConfirmationHandler implements CanHandle +{ + public function __construct(private EmailStrategy $email) {} + + public function handle(Event $event): void + { + $this->email->send( + $event->customerEmail, + 'Order Confirmation', + $this->formatEmail($event) + ); + } +} + +class ReserveInventoryHandler implements CanHandle +{ + public function __construct(private InventoryService $inventory) {} + + public function handle(Event $event): void + { + foreach ($event->items as $item) { + $this->inventory->reserve($item['sku'], $item['quantity']); + } + } +} + +class NotifyWarehouseHandler implements CanHandle +{ + public function __construct(private WarehouseClient $warehouse) {} + + public function handle(Event $event): void + { + $this->warehouse->queueForFulfillment($event->orderId); + } +} + +// Module registration +class OrderModule implements HasListeners +{ + public function getListeners(): array + { + return [ + OrderPlacedEvent::class => [ + SendOrderConfirmationHandler::class, + ReserveInventoryHandler::class, + ], + PaymentReceivedEvent::class => [ + NotifyWarehouseHandler::class, + UpdateCustomerLoyaltyHandler::class, + ], + ]; + } +} + +// Service usage +class OrderService +{ + public function __construct( + private EventStrategy $events, + private OrderRepository $orders + ) {} + + public function placeOrder(Cart $cart, Customer $customer): Order + { + $order = Order::fromCart($cart, $customer); + $this->orders->save($order); + + $this->events->broadcast(new OrderPlacedEvent( + orderId: $order->getId(), + customerId: $customer->getId(), + customerEmail: $customer->getEmail(), + total: $order->getTotal(), + items: $order->getItems(), + placedAt: new \DateTimeImmutable() + )); + + return $order; + } +} +``` + +--- + +## Related Documentation + +- [Logger Package](../../logger/introduction.md) - LoggerStrategy used in handlers for logging +- [Event Interfaces](../interfaces/introduction.md) - Core event interfaces +- [Event Listeners Guide](/core-concepts/bootstrapping/initializers/event-listeners.md) - Event listener registration