-
-
Notifications
You must be signed in to change notification settings - Fork 0
Event Usage Example
Jean-Marc Strauven edited this page Jul 31, 2025
·
2 revisions
Advanced event-driven patterns and reactive workflows with Laravel Statecraft's powerful event system.
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
External Event β State Machine β State Transition β Actions β New Events
β β β β β
User Action β Guard Check β Update State β Side Effects β Broadcast
- 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)
# 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# 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: truename: 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<?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;
}
}<?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;
}
}<?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
]
));
}
}<?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'
]
));
}
}
}<?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()
]);
}
}<?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)));
}
}<?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'
]);
}
}
}
}<?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);
}
}<?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
]));
}
}<?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());
}
}<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><?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;
}
}
}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
- π Article Publishing Example - Editorial workflows
- π¦ Order Workflow Example - E-commerce processing
- π€ User Subscription Example - Subscription management
- π― Events System - Deep dive into event handling
- π State History - Audit trails and analytics
- π§ͺ Testing Guide - Testing event-driven workflows
- βοΈ Configuration Guide - Advanced event configuration
Ready to dive deeper into the events system? Check out π― Events System for comprehensive event handling patterns!
π― Laravel Statecraft - Advanced State Machine Implementation for Laravel
Navigate: π Home | π¦ Installation | π Basic Guide | π YAML Config | π‘ Examples
Resources: π‘οΈ Guards & Actions | π― Events | π State History | π§ͺ Testing | π¨ Commands
π Community Resources:
Made with β€οΈ for the Laravel community β’ Contribute β’ License