Skip to content

Event Usage Example

Jean-Marc Strauven edited this page Jul 31, 2025 · 2 revisions

⚑ Event Usage Example

Advanced event-driven patterns and reactive workflows with Laravel Statecraft's powerful event system.

πŸ“– Overview

This example demonstrates sophisticated event-driven architecture patterns including:

  • Reactive state machines - States that respond to external events
  • Cross-model workflows - Events triggering state changes across different models
  • Event-driven automation - Automated processes triggered by state transitions
  • Real-time notifications - Live updates via WebSockets and broadcasting
  • Event sourcing patterns - Complete audit trails and replay capabilities

🎯 Event-Driven Architecture

Core Concepts

External Event β†’ State Machine β†’ State Transition β†’ Actions β†’ New Events
      ↓              ↓               ↓             ↓         ↓
   User Action β†’ Guard Check β†’ Update State β†’ Side Effects β†’ Broadcast

Event Types

  • Domain Events - Business-specific events (OrderPaid, UserVerified)
  • State Events - State machine transitions (StateTransitioned, StateChanged)
  • System Events - Infrastructure events (PaymentProcessed, EmailSent)
  • External Events - Third-party webhooks (StripeWebhook, MailgunEvent)

βš™οΈ Multi-Model Event Workflow

Order β†’ Inventory β†’ Fulfillment Chain

# Order State Machine
name: OrderStateMachine
model: App\Models\Order
initial_state: pending

# Event-driven transitions
transitions:
  - name: confirm_payment
    from: pending
    to: confirmed
    guard: PaymentConfirmedEvent
    actions: [ReserveInventoryAction, NotifyFulfillmentAction]
    
  - name: fulfill_order
    from: confirmed
    to: fulfilled
    guard: InventoryReservedEvent
    action: CreateShipmentAction

# Inventory State Machine
name: InventoryStateMachine
model: App\Models\InventoryItem
initial_state: available

transitions:
  - name: reserve_stock
    from: available
    to: reserved
    guard: OrderConfirmedEvent
    action: ReserveStockAction
    
  - name: confirm_shipment
    from: reserved
    to: shipped
    guard: ShipmentCreatedEvent
    action: UpdateStockLevelsAction

Cross-Model Event Configuration

# Global Event Configuration
event_bindings:
  # Order events trigger inventory changes
  - event: App\Events\OrderConfirmed
    target_models: [App\Models\InventoryItem]
    conditions:
      - order.items.contains(inventory_item.product_id)
    
  # Inventory events trigger fulfillment
  - event: App\Events\InventoryReserved
    target_models: [App\Models\Fulfillment]
    conditions:
      - inventory.order_id == fulfillment.order_id
    
  # Payment events trigger multiple workflows
  - event: App\Events\PaymentProcessed
    target_models: [App\Models\Order, App\Models\Subscription, App\Models\Invoice]
    parallel: true

🎭 Reactive State Machine Example

Smart Home Device Controller

name: SmartDeviceStateMachine
model: App\Models\SmartDevice
initial_state: offline
description: "IoT device state management with event reactivity"

config:
  enable_events: true
  enable_real_time: true
  event_timeout: 30 # seconds

states:
  - name: offline
    description: "Device is disconnected"
    metadata:
      color: "#DC2626"
      power_consumption: 0
      
  - name: connecting
    description: "Device is establishing connection"
    metadata:
      color: "#F59E0B"
      timeout_seconds: 30
      
  - name: online
    description: "Device is connected and idle"
    metadata:
      color: "#10B981"
      power_consumption: 5
      
  - name: active
    description: "Device is actively working"
    metadata:
      color: "#3B82F6"
      power_consumption: 15
      
  - name: maintenance
    description: "Device in maintenance mode"
    metadata:
      color: "#8B5CF6"
      power_consumption: 2
      
  - name: error
    description: "Device has encountered an error"
    metadata:
      color: "#EF4444"
      power_consumption: 1

# Event-driven transitions
transitions:
  # External events from device
  - name: device_connected
    from: [offline, connecting]
    to: online
    guard: DeviceHeartbeatEvent
    action: RegisterDeviceAction
    
  - name: start_task
    from: online
    to: active
    guard: TaskAssignedEvent
    action: BeginTaskAction
    
  - name: complete_task
    from: active
    to: online
    guard: TaskCompletedEvent
    action: ReportCompletionAction
    
  # Automatic transitions based on conditions
  - name: enter_maintenance
    from: [online, active]
    to: maintenance
    guard: MaintenanceScheduledEvent
    action: PrepareMaintenanceAction
    
  - name: handle_error
    from: [online, active, connecting]
    to: error
    guard: ErrorDetectedEvent
    action: LogErrorAction
    
  - name: recover_from_error
    from: error
    to: online
    guard: ErrorResolvedEvent
    action: ClearErrorStateAction
    
  # Connection management
  - name: disconnect
    from: [online, active, maintenance, error]
    to: offline
    guard: ConnectionLostEvent
    action: CleanupConnectionAction
    
  - name: reconnect
    from: offline
    to: connecting
    guard: ReconnectionAttemptEvent
    action: InitiateConnectionAction

🎯 Event-Driven Guards

Event-Based Guards

<?php

namespace App\Guards;

use Grazulex\LaravelStatecraft\Contracts\Guard;
use Illuminate\Database\Eloquent\Model;
use App\Events\PaymentProcessedEvent;

class PaymentConfirmedEvent implements Guard
{
    public function passes(Model $model, array $context = []): bool
    {
        // Check if this guard was triggered by the correct event
        if (!isset($context['triggered_by_event'])) {
            return false;
        }
        
        $event = $context['triggered_by_event'];
        
        // Verify it's a payment event for this order
        if (!$event instanceof PaymentProcessedEvent) {
            return false;
        }
        
        // Verify payment is for this specific order
        if ($event->payment->order_id !== $model->id) {
            return false;
        }
        
        // Verify payment amount covers order total
        if ($event->payment->amount < $model->total_amount) {
            return false;
        }
        
        // Verify payment was successful
        return $event->payment->status === 'succeeded';
    }
}

class DeviceHeartbeatEvent implements Guard
{
    public function passes(Model $model, array $context = []): bool
    {
        $event = $context['triggered_by_event'] ?? null;
        
        if (!$event instanceof \App\Events\DeviceHeartbeat) {
            return false;
        }
        
        // Verify heartbeat is from the correct device
        if ($event->deviceId !== $model->device_id) {
            return false;
        }
        
        // Verify heartbeat is recent (within last 60 seconds)
        $heartbeatTime = $event->timestamp;
        if ($heartbeatTime < now()->subSeconds(60)) {
            return false;
        }
        
        // Verify device status is healthy
        return $event->status === 'healthy';
    }
}

class InventoryReservedEvent implements Guard
{
    public function passes(Model $model, array $context = []): bool
    {
        $event = $context['triggered_by_event'] ?? null;
        
        if (!$event instanceof \App\Events\InventoryReserved) {
            return false;
        }
        
        // Check if all order items have been reserved
        foreach ($model->items as $orderItem) {
            $reserved = $event->reservations
                ->where('product_id', $orderItem->product_id)
                ->where('quantity', '>=', $orderItem->quantity)
                ->isNotEmpty();
                
            if (!$reserved) {
                return false;
            }
        }
        
        return true;
    }
}

Conditional Event Guards

<?php

namespace App\Guards;

use Grazulex\LaravelStatecraft\Contracts\Guard;
use Illuminate\Database\Eloquent\Model;

class TimeBasedEventGuard implements Guard
{
    public function passes(Model $model, array $context = []): bool
    {
        $event = $context['triggered_by_event'] ?? null;
        
        if (!$event) {
            return false;
        }
        
        $eventTime = $event->timestamp ?? now();
        
        // Only allow events during business hours
        if ($eventTime->hour < 9 || $eventTime->hour > 17) {
            return false;
        }
        
        // Don't allow events on weekends
        if ($eventTime->isWeekend()) {
            return false;
        }
        
        return true;
    }
}

class RateLimitedEventGuard implements Guard
{
    public function passes(Model $model, array $context = []): bool
    {
        $event = $context['triggered_by_event'] ?? null;
        
        if (!$event) {
            return false;
        }
        
        // Check rate limit for this event type
        $eventType = get_class($event);
        $cacheKey = "rate_limit:{$eventType}:{$model->id}";
        
        $recentEvents = cache()->get($cacheKey, 0);
        
        // Allow max 5 events per minute
        if ($recentEvents >= 5) {
            return false;
        }
        
        // Increment counter
        cache()->put($cacheKey, $recentEvents + 1, now()->addMinute());
        
        return true;
    }
}

⚑ Event-Driven Actions

Multi-Model Actions

<?php

namespace App\Actions;

use Grazulex\LaravelStatecraft\Contracts\Action;
use Illuminate\Database\Eloquent\Model;
use App\Events\InventoryReservationRequested;
use App\Events\FulfillmentRequested;

class ReserveInventoryAction implements Action
{
    public function execute(Model $model, array $context = []): void
    {
        // Trigger inventory reservation for each order item
        foreach ($model->items as $item) {
            event(new InventoryReservationRequested(
                orderId: $model->id,
                productId: $item->product_id,
                quantity: $item->quantity,
                warehouseId: $item->preferred_warehouse_id
            ));
        }
        
        // Update order with reservation request timestamp
        $model->update([
            'inventory_reservation_requested_at' => now(),
            'reservation_status' => 'pending'
        ]);
    }
}

class NotifyFulfillmentAction implements Action
{
    public function execute(Model $model, array $context = []): void
    {
        // Create fulfillment record
        $fulfillment = $model->fulfillments()->create([
            'status' => 'pending',
            'requested_at' => now(),
            'priority' => $model->priority,
            'shipping_method' => $model->shipping_method
        ]);
        
        // Trigger fulfillment workflow
        event(new FulfillmentRequested(
            orderId: $model->id,
            fulfillmentId: $fulfillment->id,
            priority: $model->priority,
            items: $model->items->toArray()
        ));
    }
}

class CreateShipmentAction implements Action
{
    public function execute(Model $model, array $context = []): void
    {
        $fulfillment = $model->fulfillments()->pending()->first();
        
        if (!$fulfillment) {
            throw new \Exception('No pending fulfillment found for order');
        }
        
        // Create shipment
        $shipment = $fulfillment->shipments()->create([
            'tracking_number' => $this->generateTrackingNumber(),
            'carrier' => $model->shipping_carrier,
            'service_type' => $model->shipping_method,
            'estimated_delivery' => $this->calculateDeliveryDate($model),
            'status' => 'created'
        ]);
        
        // Trigger shipping events
        event(new ShipmentCreated(
            shipmentId: $shipment->id,
            orderId: $model->id,
            trackingNumber: $shipment->tracking_number
        ));
        
        // Notify customer
        event(new CustomerNotificationRequested(
            type: 'shipment_created',
            orderId: $model->id,
            data: [
                'tracking_number' => $shipment->tracking_number,
                'estimated_delivery' => $shipment->estimated_delivery
            ]
        ));
    }
}

Real-Time Broadcasting Actions

<?php

namespace App\Actions;

use Grazulex\LaravelStatecraft\Contracts\Action;
use Illuminate\Database\Eloquent\Model;
use App\Events\RealTimeStateUpdate;

class BroadcastStateChangeAction implements Action
{
    public function execute(Model $model, array $context = []): void
    {
        // Broadcast to user's private channel
        broadcast(new RealTimeStateUpdate(
            modelType: get_class($model),
            modelId: $model->id,
            newState: $model->currentState(),
            previousState: $context['from_state'] ?? null,
            timestamp: now(),
            metadata: $model->getStateMetadata()
        ))->toOthers();
        
        // Also broadcast to admin dashboard
        broadcast(new RealTimeStateUpdate(
            modelType: get_class($model),
            modelId: $model->id,
            newState: $model->currentState(),
            previousState: $context['from_state'] ?? null,
            timestamp: now(),
            metadata: $model->getStateMetadata()
        ))->toChannel('admin-dashboard');
        
        // Update real-time metrics
        $this->updateRealTimeMetrics($model, $context);
    }
    
    private function updateRealTimeMetrics(Model $model, array $context): void
    {
        $modelType = str_replace('App\\Models\\', '', get_class($model));
        $newState = $model->currentState();
        
        // Update state distribution metrics
        cache()->increment("metrics:{$modelType}:states:{$newState}");
        
        if ($previousState = $context['from_state'] ?? null) {
            cache()->decrement("metrics:{$modelType}:states:{$previousState}");
        }
        
        // Update transition metrics
        $transitionKey = "metrics:{$modelType}:transitions:{$previousState}:{$newState}";
        cache()->increment($transitionKey);
    }
}

class TriggerWebhookAction implements Action
{
    public function execute(Model $model, array $context = []): void
    {
        // Get webhook configurations for this model/state
        $webhooks = $model->webhookConfigurations()
            ->where('event_type', 'state_changed')
            ->where('is_active', true)
            ->get();
        
        foreach ($webhooks as $webhook) {
            // Queue webhook delivery
            dispatch(new DeliverWebhookJob(
                url: $webhook->url,
                payload: [
                    'event' => 'state_changed',
                    'model' => [
                        'type' => get_class($model),
                        'id' => $model->id,
                        'attributes' => $model->toArray()
                    ],
                    'state' => [
                        'current' => $model->currentState(),
                        'previous' => $context['from_state'] ?? null,
                        'metadata' => $model->getStateMetadata()
                    ],
                    'timestamp' => now()->toISOString(),
                    'signature' => $this->generateSignature($webhook, $model)
                ],
                headers: [
                    'Content-Type' => 'application/json',
                    'X-Webhook-Signature' => $this->generateSignature($webhook, $model),
                    'X-Event-Type' => 'state_changed'
                ]
            ));
        }
    }
}

🎭 Event Listener Examples

Cross-Model Event Listener

<?php

namespace App\Listeners;

use App\Events\OrderConfirmed;
use App\Models\InventoryItem;
use App\Models\Fulfillment;
use App\Services\NotificationService;

class OrderConfirmedListener
{
    public function __construct(
        private NotificationService $notificationService
    ) {}

    public function handle(OrderConfirmed $event): void
    {
        $order = $event->order;
        
        // Trigger inventory reservations
        $this->reserveInventory($order);
        
        // Create fulfillment workflow
        $this->createFulfillmentWorkflow($order);
        
        // Send notifications
        $this->sendNotifications($order);
        
        // Update analytics
        $this->updateAnalytics($order);
    }
    
    private function reserveInventory($order): void
    {
        foreach ($order->items as $item) {
            // Find available inventory
            $inventoryItems = InventoryItem::where('product_id', $item->product_id)
                ->where('status', 'available')
                ->where('quantity', '>=', $item->quantity)
                ->get();
            
            foreach ($inventoryItems as $inventoryItem) {
                if ($inventoryItem->canTransition('reserve_stock')) {
                    $inventoryItem->transition('reserve_stock', [
                        'triggered_by_event' => $event,
                        'order_id' => $order->id,
                        'quantity_to_reserve' => $item->quantity
                    ]);
                    break; // Only need one inventory item per order item
                }
            }
        }
    }
    
    private function createFulfillmentWorkflow($order): void
    {
        $fulfillment = Fulfillment::create([
            'order_id' => $order->id,
            'status' => 'pending',
            'priority' => $order->priority,
            'warehouse_id' => $order->preferred_warehouse_id
        ]);
        
        // Trigger fulfillment state machine
        if ($fulfillment->canTransition('start_fulfillment')) {
            $fulfillment->transition('start_fulfillment', [
                'triggered_by_event' => $event
            ]);
        }
    }
    
    private function sendNotifications($order): void
    {
        // Customer notification
        $this->notificationService->send($order->customer, 'order_confirmed', [
            'order' => $order,
            'estimated_delivery' => $order->estimated_delivery_date
        ]);
        
        // Internal notifications
        $this->notificationService->notify('fulfillment_team', 'new_order', [
            'order_id' => $order->id,
            'priority' => $order->priority,
            'item_count' => $order->items->count()
        ]);
    }
}

Real-Time Event Listener

<?php

namespace App\Listeners;

use Grazulex\LaravelStatecraft\Events\StateTransitioned;
use App\Events\RealTimeUpdate;
use Illuminate\Support\Facades\Redis;

class RealTimeStateListener
{
    public function handle(StateTransitioned $event): void
    {
        $model = $event->model;
        $modelType = class_basename($model);
        
        // Update real-time dashboard
        $this->updateDashboard($model, $event);
        
        // Update live metrics
        $this->updateLiveMetrics($model, $event);
        
        // Trigger real-time notifications
        $this->triggerRealTimeNotifications($model, $event);
        
        // Update WebSocket connections
        $this->updateWebSocketClients($model, $event);
    }
    
    private function updateDashboard($model, $event): void
    {
        broadcast(new RealTimeUpdate([
            'type' => 'state_change',
            'model_type' => class_basename($model),
            'model_id' => $model->id,
            'old_state' => $event->from,
            'new_state' => $event->to,
            'timestamp' => now(),
            'user_id' => auth()->id()
        ]))->toChannel('admin-dashboard');
    }
    
    private function updateLiveMetrics($model, $event): void
    {
        $modelType = class_basename($model);
        
        // Use Redis for real-time counters
        Redis::pipeline(function ($pipe) use ($modelType, $event) {
            // Increment new state counter
            $pipe->hincrby("live_metrics:{$modelType}:states", $event->to, 1);
            
            // Decrement old state counter
            $pipe->hincrby("live_metrics:{$modelType}:states", $event->from, -1);
            
            // Track transition
            $pipe->hincrby(
                "live_metrics:{$modelType}:transitions", 
                "{$event->from}:{$event->to}", 
                1
            );
            
            // Set expiry for metrics
            $pipe->expire("live_metrics:{$modelType}:states", 3600);
            $pipe->expire("live_metrics:{$modelType}:transitions", 3600);
        });
    }
    
    private function triggerRealTimeNotifications($model, $event): void
    {
        // Critical state changes trigger immediate notifications
        $criticalStates = ['error', 'failed', 'suspended', 'cancelled'];
        
        if (in_array($event->to, $criticalStates)) {
            broadcast(new RealTimeAlert([
                'level' => 'critical',
                'message' => "{$model->getDisplayName()} entered {$event->to} state",
                'model_type' => class_basename($model),
                'model_id' => $model->id,
                'action_required' => true
            ]))->toChannel('alerts');
        }
    }
    
    private function updateWebSocketClients($model, $event): void
    {
        // Update clients subscribed to this specific model
        broadcast(new ModelStateChanged([
            'model_type' => class_basename($model),
            'model_id' => $model->id,
            'state' => $event->to,
            'metadata' => $model->getStateMetadata(),
            'timestamp' => now()
        ]))->toChannel("model.{$model->id}");
        
        // Update clients subscribed to this model type
        broadcast(new ModelStateChanged([
            'model_type' => class_basename($model),
            'model_id' => $model->id,
            'state' => $event->to,
            'metadata' => $model->getStateMetadata(),
            'timestamp' => now()
        ]))->toChannel("models." . strtolower(class_basename($model)));
    }
}

🎯 External Event Integration

Webhook Event Processor

<?php

namespace App\Services;

use App\Models\Order;
use App\Models\Subscription;
use App\Events\PaymentProcessedEvent;
use App\Events\WebhookReceivedEvent;

class WebhookEventProcessor
{
    public function processStripeWebhook(array $payload): void
    {
        $eventType = $payload['type'];
        $data = $payload['data']['object'];
        
        switch ($eventType) {
            case 'payment_intent.succeeded':
                $this->handlePaymentSuccess($data);
                break;
                
            case 'payment_intent.payment_failed':
                $this->handlePaymentFailure($data);
                break;
                
            case 'invoice.payment_succeeded':
                $this->handleSubscriptionPayment($data);
                break;
                
            case 'customer.subscription.deleted':
                $this->handleSubscriptionCancellation($data);
                break;
        }
    }
    
    private function handlePaymentSuccess(array $paymentData): void
    {
        // Find related order or subscription
        $metadata = $paymentData['metadata'];
        
        if (isset($metadata['order_id'])) {
            $order = Order::find($metadata['order_id']);
            
            if ($order && $order->canTransition('confirm_payment')) {
                $order->transition('confirm_payment', [
                    'triggered_by_event' => new PaymentProcessedEvent(
                        paymentId: $paymentData['id'],
                        amount: $paymentData['amount'] / 100, // Convert from cents
                        currency: $paymentData['currency'],
                        status: 'succeeded',
                        orderId: $metadata['order_id']
                    ),
                    'payment_intent_id' => $paymentData['id'],
                    'amount_received' => $paymentData['amount'] / 100
                ]);
            }
        }
        
        if (isset($metadata['subscription_id'])) {
            $subscription = Subscription::find($metadata['subscription_id']);
            
            if ($subscription && $subscription->canTransition('activate_subscription')) {
                $subscription->transition('activate_subscription', [
                    'triggered_by_event' => new PaymentProcessedEvent(
                        paymentId: $paymentData['id'],
                        amount: $paymentData['amount'] / 100,
                        currency: $paymentData['currency'],
                        status: 'succeeded',
                        subscriptionId: $metadata['subscription_id']
                    ),
                    'payment_intent_id' => $paymentData['id']
                ]);
            }
        }
    }
    
    private function handlePaymentFailure(array $paymentData): void
    {
        $metadata = $paymentData['metadata'];
        
        if (isset($metadata['subscription_id'])) {
            $subscription = Subscription::find($metadata['subscription_id']);
            
            if ($subscription && $subscription->canTransition('suspend_for_payment')) {
                $subscription->transition('suspend_for_payment', [
                    'triggered_by_event' => new PaymentProcessedEvent(
                        paymentId: $paymentData['id'],
                        amount: $paymentData['amount'] / 100,
                        currency: $paymentData['currency'],
                        status: 'failed',
                        subscriptionId: $metadata['subscription_id']
                    ),
                    'failure_reason' => $paymentData['last_payment_error']['message'] ?? 'Payment failed'
                ]);
            }
        }
    }
}

IoT Device Event Handler

<?php

namespace App\Services;

use App\Models\SmartDevice;
use App\Events\DeviceHeartbeat;
use App\Events\TaskAssigned;
use App\Events\ErrorDetected;

class IoTEventHandler
{
    public function handleDeviceMessage(string $deviceId, array $message): void
    {
        $device = SmartDevice::where('device_id', $deviceId)->first();
        
        if (!$device) {
            logger()->warning("Unknown device message received: {$deviceId}");
            return;
        }
        
        $messageType = $message['type'];
        
        switch ($messageType) {
            case 'heartbeat':
                $this->handleHeartbeat($device, $message);
                break;
                
            case 'task_completed':
                $this->handleTaskCompletion($device, $message);
                break;
                
            case 'error':
                $this->handleDeviceError($device, $message);
                break;
                
            case 'status_update':
                $this->handleStatusUpdate($device, $message);
                break;
        }
    }
    
    private function handleHeartbeat(SmartDevice $device, array $message): void
    {
        if ($device->canTransition('device_connected')) {
            $device->transition('device_connected', [
                'triggered_by_event' => new DeviceHeartbeat(
                    deviceId: $device->device_id,
                    timestamp: now(),
                    status: $message['status'],
                    batteryLevel: $message['battery_level'] ?? null,
                    signalStrength: $message['signal_strength'] ?? null
                )
            ]);
        }
        
        // Update device metrics
        $device->update([
            'last_heartbeat_at' => now(),
            'battery_level' => $message['battery_level'] ?? $device->battery_level,
            'signal_strength' => $message['signal_strength'] ?? $device->signal_strength
        ]);
    }
    
    private function handleTaskCompletion(SmartDevice $device, array $message): void
    {
        if ($device->canTransition('complete_task')) {
            $device->transition('complete_task', [
                'triggered_by_event' => new TaskCompleted(
                    deviceId: $device->device_id,
                    taskId: $message['task_id'],
                    completedAt: now(),
                    result: $message['result'] ?? null
                ),
                'task_id' => $message['task_id'],
                'completion_time' => $message['duration'] ?? null
            ]);
        }
    }
    
    private function handleDeviceError(SmartDevice $device, array $message): void
    {
        if ($device->canTransition('handle_error')) {
            $device->transition('handle_error', [
                'triggered_by_event' => new ErrorDetected(
                    deviceId: $device->device_id,
                    errorCode: $message['error_code'],
                    errorMessage: $message['error_message'],
                    severity: $message['severity'] ?? 'medium',
                    timestamp: now()
                ),
                'error_code' => $message['error_code'],
                'error_message' => $message['error_message']
            ]);
        }
        
        // Log error for analysis
        logger()->error("Device error: {$device->device_id}", $message);
    }
}

πŸ§ͺ Testing Event-Driven Workflows

Event-Driven Feature Tests

<?php

namespace Tests\Feature;

use App\Models\Order;
use App\Models\InventoryItem;
use App\Events\PaymentProcessedEvent;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;

class EventDrivenWorkflowTest extends TestCase
{
    use RefreshDatabase;
    
    public function test_payment_event_triggers_order_confirmation()
    {
        Event::fake();
        
        $order = Order::factory()->pending()->create();
        $this->assertEquals('pending', $order->currentState());
        
        // Simulate payment success event
        $paymentEvent = new PaymentProcessedEvent(
            paymentId: 'pi_test123',
            amount: $order->total_amount,
            currency: 'usd',
            status: 'succeeded',
            orderId: $order->id
        );
        
        // Transition should work with event context
        $order->transition('confirm_payment', [
            'triggered_by_event' => $paymentEvent
        ]);
        
        $this->assertEquals('confirmed', $order->currentState());
        
        // Verify event was dispatched
        Event::assertDispatched(\App\Events\OrderConfirmed::class);
    }
    
    public function test_cross_model_event_workflow()
    {
        Event::fake();
        
        $order = Order::factory()->pending()->create();
        $inventoryItem = InventoryItem::factory()->available()->create([
            'product_id' => $order->items->first()->product_id,
            'quantity' => 100
        ]);
        
        // Confirm order payment
        $order->transition('confirm_payment', [
            'triggered_by_event' => new PaymentProcessedEvent(
                paymentId: 'pi_test123',
                amount: $order->total_amount,
                currency: 'usd',
                status: 'succeeded',
                orderId: $order->id
            )
        ]);
        
        // Manually trigger the cross-model event (in real app this would be automatic)
        $inventoryItem->transition('reserve_stock', [
            'triggered_by_event' => new \App\Events\OrderConfirmed($order),
            'order_id' => $order->id,
            'quantity_to_reserve' => $order->items->first()->quantity
        ]);
        
        $this->assertEquals('confirmed', $order->currentState());
        $this->assertEquals('reserved', $inventoryItem->currentState());
    }
    
    public function test_event_guard_validation()
    {
        $order = Order::factory()->pending()->create();
        
        // Try to confirm without proper event - should fail
        $this->assertFalse($order->canTransition('confirm_payment'));
        
        // Try with wrong event type - should fail
        $wrongEvent = new \App\Events\TaskCompleted($order);
        $this->assertFalse($order->canTransition('confirm_payment', [
            'triggered_by_event' => $wrongEvent
        ]));
        
        // Try with correct event - should succeed
        $correctEvent = new PaymentProcessedEvent(
            paymentId: 'pi_test123',
            amount: $order->total_amount,
            currency: 'usd',
            status: 'succeeded',
            orderId: $order->id
        );
        
        $this->assertTrue($order->canTransition('confirm_payment', [
            'triggered_by_event' => $correctEvent
        ]));
    }
}

Event Integration Tests

<?php

namespace Tests\Integration;

use App\Models\SmartDevice;
use App\Events\DeviceHeartbeat;
use App\Services\IoTEventHandler;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;

class IoTEventIntegrationTest extends TestCase
{
    use RefreshDatabase;
    
    public function test_device_heartbeat_integration()
    {
        $device = SmartDevice::factory()->offline()->create();
        $eventHandler = app(IoTEventHandler::class);
        
        // Simulate heartbeat message
        $message = [
            'type' => 'heartbeat',
            'status' => 'healthy',
            'battery_level' => 85,
            'signal_strength' => -45,
            'timestamp' => now()->toISOString()
        ];
        
        $eventHandler->handleDeviceMessage($device->device_id, $message);
        
        $device->refresh();
        $this->assertEquals('online', $device->currentState());
        $this->assertEquals(85, $device->battery_level);
        $this->assertNotNull($device->last_heartbeat_at);
    }
    
    public function test_device_error_handling()
    {
        $device = SmartDevice::factory()->online()->create();
        $eventHandler = app(IoTEventHandler::class);
        
        // Simulate error message
        $message = [
            'type' => 'error',
            'error_code' => 'SENSOR_FAILURE',
            'error_message' => 'Temperature sensor not responding',
            'severity' => 'high'
        ];
        
        $eventHandler->handleDeviceMessage($device->device_id, $message);
        
        $device->refresh();
        $this->assertEquals('error', $device->currentState());
    }
}

πŸ“Š Real-Time Dashboard Integration

Vue.js Dashboard Component

<template>
  <div class="real-time-dashboard">
    <div class="metrics-grid">
      <div v-for="metric in metrics" :key="metric.name" class="metric-card">
        <h3>{{ metric.label }}</h3>
        <div class="metric-value">{{ metric.value }}</div>
        <div class="metric-trend" :class="metric.trend">
          {{ metric.change }}
        </div>
      </div>
    </div>
    
    <div class="state-distribution">
      <h3>State Distribution</h3>
      <div class="state-chart">
        <div 
          v-for="state in stateDistribution" 
          :key="state.name"
          class="state-bar"
          :style="{ width: state.percentage + '%', backgroundColor: state.color }"
        >
          {{ state.name }}: {{ state.count }}
        </div>
      </div>
    </div>
    
    <div class="recent-transitions">
      <h3>Recent Transitions</h3>
      <div class="transition-list">
        <div 
          v-for="transition in recentTransitions" 
          :key="transition.id"
          class="transition-item"
        >
          <span class="model">{{ transition.model_type }}</span>
          <span class="states">
            {{ transition.from_state }} β†’ {{ transition.to_state }}
          </span>
          <span class="time">{{ formatTime(transition.timestamp) }}</span>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'RealTimeDashboard',
  
  data() {
    return {
      metrics: [],
      stateDistribution: [],
      recentTransitions: [],
      websocketConnection: null
    }
  },
  
  mounted() {
    this.connectWebSocket();
    this.loadInitialData();
  },
  
  methods: {
    connectWebSocket() {
      this.websocketConnection = Echo.channel('admin-dashboard')
        .listen('RealTimeUpdate', (event) => {
          this.handleRealTimeUpdate(event);
        })
        .listen('StateTransitioned', (event) => {
          this.handleStateTransition(event);
        });
    },
    
    handleRealTimeUpdate(event) {
      // Update metrics
      this.updateMetrics(event);
      
      // Update state distribution
      this.updateStateDistribution(event);
      
      // Add to recent transitions
      this.addRecentTransition(event);
    },
    
    updateMetrics(event) {
      // Update real-time metrics based on event
      const metricIndex = this.metrics.findIndex(m => m.name === event.metric);
      if (metricIndex !== -1) {
        this.metrics[metricIndex].value = event.value;
        this.metrics[metricIndex].change = event.change;
        this.metrics[metricIndex].trend = event.trend;
      }
    },
    
    updateStateDistribution(event) {
      if (event.type === 'state_change') {
        // Update state counts
        const oldStateIndex = this.stateDistribution.findIndex(
          s => s.name === event.old_state
        );
        const newStateIndex = this.stateDistribution.findIndex(
          s => s.name === event.new_state
        );
        
        if (oldStateIndex !== -1) {
          this.stateDistribution[oldStateIndex].count--;
        }
        
        if (newStateIndex !== -1) {
          this.stateDistribution[newStateIndex].count++;
        }
        
        // Recalculate percentages
        this.recalculatePercentages();
      }
    },
    
    addRecentTransition(event) {
      this.recentTransitions.unshift({
        id: Date.now(),
        model_type: event.model_type,
        from_state: event.old_state,
        to_state: event.new_state,
        timestamp: event.timestamp
      });
      
      // Keep only last 20 transitions
      this.recentTransitions = this.recentTransitions.slice(0, 20);
    }
  }
}
</script>

πŸš€ Advanced Patterns

Event Sourcing Integration

<?php

namespace App\Services;

use App\Models\EventStore;
use Illuminate\Database\Eloquent\Model;

class EventSourcingService
{
    public function recordStateTransition(Model $model, string $from, string $to, array $context = []): void
    {
        EventStore::create([
            'aggregate_type' => get_class($model),
            'aggregate_id' => $model->id,
            'event_type' => 'state_transitioned',
            'event_data' => [
                'from_state' => $from,
                'to_state' => $to,
                'context' => $context,
                'user_id' => auth()->id(),
                'timestamp' => now(),
                'model_snapshot' => $model->toArray()
            ],
            'occurred_at' => now()
        ]);
    }
    
    public function replayEventsForModel(string $modelClass, int $modelId): Model
    {
        $events = EventStore::where('aggregate_type', $modelClass)
            ->where('aggregate_id', $modelId)
            ->orderBy('occurred_at')
            ->get();
        
        // Create fresh model instance
        $model = new $modelClass();
        $model->id = $modelId;
        
        // Replay all events
        foreach ($events as $event) {
            $this->applyEvent($model, $event);
        }
        
        return $model;
    }
    
    private function applyEvent(Model $model, EventStore $event): void
    {
        switch ($event->event_type) {
            case 'state_transitioned':
                $data = $event->event_data;
                $model->forceFill(['status' => $data['to_state']]);
                break;
                
            case 'model_created':
                $model->forceFill($event->event_data['attributes']);
                break;
                
            case 'model_updated':
                $model->forceFill($event->event_data['new_attributes']);
                break;
        }
    }
}

πŸš€ Next Steps

This comprehensive event-driven example demonstrates:

  • Reactive state machines that respond to external events
  • Cross-model workflows with event-driven coordination
  • Real-time broadcasting and live dashboard updates
  • External integrations via webhooks and IoT devices
  • Event sourcing patterns for complete audit trails

Explore More Examples

Advanced Topics

Ready to dive deeper into the events system? Check out 🎯 Events System for comprehensive event handling patterns!

Clone this wiki locally