-
-
Notifications
You must be signed in to change notification settings - Fork 0
Event Usage Example
This example demonstrates advanced event handling and integration patterns with Laravel Statecraft. It shows how to build event-driven architectures, implement real-time notifications, integrate with external systems, and create comprehensive audit trails.
The Event Usage Example covers:
- Real-time Notifications - Broadcast state changes to users
- Webhook Integrations - Send updates to external systems
- Analytics & Metrics - Track state machine performance
- Audit Logging - Comprehensive change tracking
- External System Integration - Connect with third-party services
- Custom Event Handling - Create domain-specific event patterns
# resources/state-machines/EventDemoStateMachine.yaml
name: EventDemoStateMachine
model: App\Models\EventDemo
initial_state: created
field: status
description: "Demonstration of event-driven state machine patterns"
states:
- name: created
description: Item has been created
metadata:
color: blue
- name: processing
description: Item is being processed
metadata:
color: yellow
- name: completed
description: Item processing completed
metadata:
color: green
final: true
- name: failed
description: Item processing failed
metadata:
color: red
final: true
transitions:
- name: start_processing
from: created
to: processing
action:
- StartProcessingAction
- NotifyStartedAction
metadata:
event_tags: ["processing", "started"]
- name: complete
from: processing
to: completed
action:
- CompleteProcessingAction
- NotifyCompletedAction
- UpdateMetricsAction
metadata:
event_tags: ["processing", "completed", "success"]
- name: fail
from: processing
to: failed
action:
- FailProcessingAction
- NotifyFailedAction
- LogErrorAction
metadata:
event_tags: ["processing", "failed", "error"]
<?php
namespace App\Listeners;
use Grazulex\LaravelStatecraft\Events\StateTransitioned;
use App\Services\NotificationService;
use App\Services\AnalyticsService;
use App\Services\WebhookService;
use App\Services\AuditService;
use Illuminate\Support\Facades\Log;
class StateChangeEventListener
{
public function __construct(
private NotificationService $notifications,
private AnalyticsService $analytics,
private WebhookService $webhooks,
private AuditService $audit
) {}
public function handle(StateTransitioned $event): void
{
$model = $event->model;
$fromState = $event->fromState;
$toState = $event->toState;
$transition = $event->transition;
$context = $event->context;
// Log the state change
Log::info("State transition occurred", [
'model' => get_class($model),
'model_id' => $model->getKey(),
'from_state' => $fromState,
'to_state' => $toState,
'transition' => $transition,
'user_id' => auth()->id(),
'timestamp' => now(),
]);
// Track analytics
$this->trackAnalytics($model, $fromState, $toState, $transition);
// Send real-time notifications
$this->sendRealTimeNotifications($model, $fromState, $toState);
// Send webhooks to external systems
$this->sendWebhooks($model, $fromState, $toState, $context);
// Create audit trail
$this->createAuditTrail($model, $fromState, $toState, $transition, $context);
// Handle model-specific events
$this->handleModelSpecificEvents($model, $fromState, $toState);
}
private function trackAnalytics($model, $fromState, $toState, $transition): void
{
$this->analytics->track('state_transition', [
'model_type' => get_class($model),
'model_id' => $model->getKey(),
'from_state' => $fromState,
'to_state' => $toState,
'transition_name' => $transition,
'user_id' => auth()->id(),
'session_id' => session()->getId(),
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
'occurred_at' => now(),
]);
// Track state durations
if ($model->stateHistory()->exists()) {
$lastChange = $model->stateHistory()
->orderBy('transitioned_at', 'desc')
->skip(1)
->first();
if ($lastChange) {
$duration = $lastChange->transitioned_at->diffInSeconds(now());
$this->analytics->track('state_duration', [
'model_type' => get_class($model),
'state' => $fromState,
'duration_seconds' => $duration,
]);
}
}
}
private function sendRealTimeNotifications($model, $fromState, $toState): void
{
// Broadcast to model owner
if (method_exists($model, 'user') && $model->user) {
$this->notifications->sendToUser($model->user, [
'type' => 'state_changed',
'title' => 'Status Updated',
'message' => "Your {$this->getModelDisplayName($model)} changed from {$fromState} to {$toState}",
'model_type' => get_class($model),
'model_id' => $model->getKey(),
'new_state' => $toState,
]);
}
// Broadcast to administrators
if ($this->isImportantTransition($fromState, $toState)) {
$this->notifications->sendToAdmins([
'type' => 'important_state_change',
'title' => 'Important Status Change',
'message' => "{$this->getModelDisplayName($model)} #{$model->getKey()} changed to {$toState}",
'model_type' => get_class($model),
'model_id' => $model->getKey(),
'requires_attention' => $toState === 'failed',
]);
}
}
private function sendWebhooks($model, $fromState, $toState, $context): void
{
$payload = [
'event' => 'state_transition',
'model' => [
'type' => get_class($model),
'id' => $model->getKey(),
'data' => $model->toArray(),
],
'transition' => [
'from' => $fromState,
'to' => $toState,
'context' => $context,
'occurred_at' => now()->toISOString(),
],
'user' => auth()->user()?->only(['id', 'name', 'email']),
];
// Send to configured webhooks
$this->webhooks->send('state_transition', $payload);
// Send to model-specific webhooks
if (method_exists($model, 'getWebhookUrls')) {
foreach ($model->getWebhookUrls() as $url) {
$this->webhooks->sendTo($url, $payload);
}
}
}
private function createAuditTrail($model, $fromState, $toState, $transition, $context): void
{
$this->audit->log([
'event_type' => 'state_transition',
'model_type' => get_class($model),
'model_id' => $model->getKey(),
'from_state' => $fromState,
'to_state' => $toState,
'transition_name' => $transition,
'context' => $context,
'user_id' => auth()->id(),
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
'metadata' => [
'model_snapshot' => $model->toArray(),
'route' => request()->route()?->getName(),
'request_id' => request()->header('X-Request-ID'),
],
]);
}
private function handleModelSpecificEvents($model, $fromState, $toState): void
{
// Handle different model types with specific logic
match (get_class($model)) {
\App\Models\Order::class => $this->handleOrderEvents($model, $fromState, $toState),
\App\Models\Article::class => $this->handleArticleEvents($model, $fromState, $toState),
\App\Models\Subscription::class => $this->handleSubscriptionEvents($model, $fromState, $toState),
default => null,
};
}
private function handleOrderEvents($order, $fromState, $toState): void
{
match ($toState) {
'paid' => $this->handleOrderPaid($order),
'shipped' => $this->handleOrderShipped($order),
'delivered' => $this->handleOrderDelivered($order),
'cancelled' => $this->handleOrderCancelled($order),
default => null,
};
}
private function handleOrderPaid($order): void
{
// Update customer lifetime value
$order->customer->increment('lifetime_value', $order->amount);
// Trigger inventory allocation
dispatch(\App\Jobs\AllocateInventoryJob::class, $order);
// Send to fulfillment system
$this->webhooks->send('order_paid', [
'order_id' => $order->id,
'customer_id' => $order->customer_id,
'amount' => $order->amount,
'items' => $order->items->toArray(),
]);
}
private function isImportantTransition($fromState, $toState): bool
{
$importantStates = ['failed', 'cancelled', 'suspended', 'rejected'];
return in_array($toState, $importantStates);
}
private function getModelDisplayName($model): string
{
return match (get_class($model)) {
\App\Models\Order::class => 'order',
\App\Models\Article::class => 'article',
\App\Models\Subscription::class => 'subscription',
default => 'item',
};
}
}
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ModelStateChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public $model,
public string $fromState,
public string $toState,
public string $transition,
public array $context = []
) {}
public function broadcastOn(): array
{
$channels = [
// Public channel for general updates
new Channel('state-changes'),
];
// Private channel for model owner
if (method_exists($this->model, 'user') && $this->model->user) {
$channels[] = new PrivateChannel('user.' . $this->model->user->id);
}
// Admin channel for important changes
if ($this->isImportantChange()) {
$channels[] = new PrivateChannel('admin-notifications');
}
return $channels;
}
public function broadcastAs(): string
{
return 'model.state.changed';
}
public function broadcastWith(): array
{
return [
'model_type' => get_class($this->model),
'model_id' => $this->model->getKey(),
'from_state' => $this->fromState,
'to_state' => $this->toState,
'transition' => $this->transition,
'timestamp' => now()->toISOString(),
'user' => auth()->user()?->only(['id', 'name']),
'context' => $this->context,
];
}
private function isImportantChange(): bool
{
return in_array($this->toState, ['failed', 'cancelled', 'suspended']);
}
}
// resources/js/state-machine-events.js
import Echo from 'laravel-echo';
class StateMachineEventHandler {
constructor() {
this.setupEventListeners();
}
setupEventListeners() {
// Listen to public state changes
Echo.channel('state-changes')
.listen('.model.state.changed', (event) => {
this.handleStateChange(event);
});
// Listen to user-specific changes
if (window.Laravel.user) {
Echo.private(`user.${window.Laravel.user.id}`)
.listen('.model.state.changed', (event) => {
this.handleUserStateChange(event);
});
}
// Admin notifications
if (window.Laravel.user?.is_admin) {
Echo.private('admin-notifications')
.listen('.model.state.changed', (event) => {
this.handleAdminNotification(event);
});
}
}
handleStateChange(event) {
console.log('State change:', event);
// Update UI elements
this.updateModelStatus(event.model_type, event.model_id, event.to_state);
// Show toast notification
this.showNotification(`${event.model_type} #${event.model_id} changed to ${event.to_state}`);
// Update progress indicators
this.updateProgressIndicators(event);
}
handleUserStateChange(event) {
// More prominent notifications for user's own models
this.showUserNotification(event);
// Update dashboard counters
this.updateDashboardCounters(event);
// Refresh relevant sections
this.refreshModelSections(event.model_type);
}
handleAdminNotification(event) {
if (['failed', 'cancelled', 'suspended'].includes(event.to_state)) {
this.showAdminAlert(event);
}
}
updateModelStatus(modelType, modelId, newState) {
// Find and update status elements
const statusElements = document.querySelectorAll(
`[data-model="${modelType}"][data-model-id="${modelId}"] .status`
);
statusElements.forEach(element => {
element.textContent = newState;
element.className = `status status-${newState}`;
});
}
showNotification(message) {
// Implementation depends on your notification system
// e.g., Toastr, SweetAlert, custom notifications
if (window.toastr) {
toastr.info(message);
}
}
showUserNotification(event) {
const message = `Your ${event.model_type} #${event.model_id} is now ${event.to_state}`;
// Show as browser notification if permission granted
if (Notification.permission === 'granted') {
new Notification('Status Update', {
body: message,
icon: '/icons/notification.png'
});
}
this.showNotification(message);
}
showAdminAlert(event) {
// More prominent alert for admin attention
if (window.Swal) {
Swal.fire({
title: 'Attention Required',
text: `${event.model_type} #${event.model_id} ${event.to_state}`,
icon: 'warning',
confirmButtonText: 'View Details'
});
}
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
new StateMachineEventHandler();
});
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
class WebhookService
{
public function send(string $eventType, array $payload): void
{
$webhooks = $this->getWebhooksForEvent($eventType);
foreach ($webhooks as $webhook) {
$this->sendWebhook($webhook, $payload);
}
}
public function sendTo(string $url, array $payload): void
{
$webhook = [
'url' => $url,
'secret' => config('webhooks.default_secret'),
'retry_attempts' => 3,
'timeout' => 30,
];
$this->sendWebhook($webhook, $payload);
}
private function sendWebhook(array $webhook, array $payload): void
{
$url = $webhook['url'];
$secret = $webhook['secret'] ?? '';
$maxAttempts = $webhook['retry_attempts'] ?? 3;
$timeout = $webhook['timeout'] ?? 30;
$headers = [
'Content-Type' => 'application/json',
'User-Agent' => 'Laravel-Statecraft-Webhook/1.0',
'X-Webhook-Timestamp' => now()->timestamp,
];
if ($secret) {
$headers['X-Webhook-Signature'] = $this->generateSignature($payload, $secret);
}
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
try {
$response = Http::timeout($timeout)
->withHeaders($headers)
->post($url, $payload);
if ($response->successful()) {
Log::info("Webhook sent successfully", [
'url' => $url,
'attempt' => $attempt,
'status' => $response->status(),
]);
return;
}
Log::warning("Webhook failed", [
'url' => $url,
'attempt' => $attempt,
'status' => $response->status(),
'response' => $response->body(),
]);
} catch (\Exception $e) {
Log::error("Webhook error", [
'url' => $url,
'attempt' => $attempt,
'error' => $e->getMessage(),
]);
}
if ($attempt < $maxAttempts) {
// Exponential backoff
sleep(pow(2, $attempt - 1));
}
}
// All attempts failed - queue for later retry
$this->queueFailedWebhook($url, $payload);
}
private function generateSignature(array $payload, string $secret): string
{
return hash_hmac('sha256', json_encode($payload), $secret);
}
private function getWebhooksForEvent(string $eventType): array
{
return Cache::remember("webhooks.{$eventType}", 300, function () use ($eventType) {
return config("webhooks.events.{$eventType}", []);
});
}
private function queueFailedWebhook(string $url, array $payload): void
{
// Queue webhook for retry later
dispatch(\App\Jobs\RetryWebhookJob::class, [
'url' => $url,
'payload' => $payload,
])->delay(now()->addMinutes(5));
}
}
<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
class StateMachineAnalyticsService
{
public function getTransitionMetrics(string $modelType, int $days = 30): array
{
$cacheKey = "state_metrics.{$modelType}.{$days}";
return Cache::remember($cacheKey, 3600, function () use ($modelType, $days) {
$startDate = now()->subDays($days);
return [
'transition_counts' => $this->getTransitionCounts($modelType, $startDate),
'state_distribution' => $this->getStateDistribution($modelType),
'average_durations' => $this->getAverageDurations($modelType, $startDate),
'success_rates' => $this->getSuccessRates($modelType, $startDate),
'daily_transitions' => $this->getDailyTransitions($modelType, $startDate),
];
});
}
private function getTransitionCounts(string $modelType, $startDate): array
{
return DB::table('state_machine_histories')
->where('model_type', $modelType)
->where('transitioned_at', '>=', $startDate)
->selectRaw('transition, COUNT(*) as count')
->groupBy('transition')
->orderBy('count', 'desc')
->pluck('count', 'transition')
->toArray();
}
private function getStateDistribution(string $modelType): array
{
$tableName = $this->getTableNameForModel($modelType);
return DB::table($tableName)
->selectRaw('status, COUNT(*) as count')
->groupBy('status')
->pluck('count', 'status')
->toArray();
}
private function getAverageDurations(string $modelType, $startDate): array
{
$durations = [];
$transitions = DB::table('state_machine_histories')
->where('model_type', $modelType)
->where('transitioned_at', '>=', $startDate)
->orderBy('model_id')
->orderBy('transitioned_at')
->get();
$modelTransitions = $transitions->groupBy('model_id');
foreach ($modelTransitions as $modelId => $modelTransitions) {
$previous = null;
foreach ($modelTransitions as $transition) {
if ($previous) {
$duration = strtotime($transition->transitioned_at) - strtotime($previous->transitioned_at);
$durations[$previous->to_state][] = $duration;
}
$previous = $transition;
}
}
return array_map(function ($stateDurations) {
return array_sum($stateDurations) / count($stateDurations);
}, $durations);
}
private function getSuccessRates(string $modelType, $startDate): array
{
$totalModels = DB::table($this->getTableNameForModel($modelType))
->where('created_at', '>=', $startDate)
->count();
$successfulModels = DB::table($this->getTableNameForModel($modelType))
->where('created_at', '>=', $startDate)
->whereIn('status', ['completed', 'delivered', 'published', 'active'])
->count();
$failedModels = DB::table($this->getTableNameForModel($modelType))
->where('created_at', '>=', $startDate)
->whereIn('status', ['failed', 'cancelled', 'rejected'])
->count();
return [
'total' => $totalModels,
'successful' => $successfulModels,
'failed' => $failedModels,
'success_rate' => $totalModels > 0 ? ($successfulModels / $totalModels) * 100 : 0,
'failure_rate' => $totalModels > 0 ? ($failedModels / $totalModels) * 100 : 0,
];
}
private function getDailyTransitions(string $modelType, $startDate): array
{
return DB::table('state_machine_histories')
->where('model_type', $modelType)
->where('transitioned_at', '>=', $startDate)
->selectRaw('DATE(transitioned_at) as date, COUNT(*) as count')
->groupBy('date')
->orderBy('date')
->pluck('count', 'date')
->toArray();
}
private function getTableNameForModel(string $modelType): string
{
// Convert model class to table name
$model = new $modelType;
return $model->getTable();
}
}
<?php
namespace Tests\Feature;
use App\Models\EventDemo;
use App\Events\ModelStateChanged;
use Grazulex\LaravelStatecraft\Events\StateTransitioned;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class EventIntegrationTest extends TestCase
{
public function test_state_change_fires_events()
{
Event::fake();
$model = EventDemo::factory()->create();
$model->transitionTo('start_processing');
// Assert Laravel Statecraft event was fired
Event::assertDispatched(StateTransitioned::class, function ($event) use ($model) {
return $event->model->is($model) &&
$event->fromState === 'created' &&
$event->toState === 'processing';
});
// Assert custom event was fired
Event::assertDispatched(ModelStateChanged::class);
}
public function test_webhooks_are_sent_on_state_change()
{
Http::fake();
config(['webhooks.events.state_transition' => [
['url' => 'https://external-api.com/webhook', 'secret' => 'secret123']
]]);
$model = EventDemo::factory()->create();
$model->transitionTo('complete');
Http::assertSent(function ($request) {
return $request->url() === 'https://external-api.com/webhook' &&
$request->hasHeader('X-Webhook-Signature');
});
}
public function test_analytics_are_tracked()
{
$model = EventDemo::factory()->create();
// Transition through multiple states
$model->transitionTo('start_processing');
$model->transitionTo('complete');
// Check analytics were recorded
$this->assertDatabaseHas('analytics_events', [
'event_type' => 'state_transition',
'model_type' => EventDemo::class,
'model_id' => $model->id,
]);
}
}
This Event Usage example showcases:
- Comprehensive Event Handling - Multiple event types and listeners
- Real-time Broadcasting - Live updates to frontend applications
- Webhook Integration - Reliable external system notifications
- Analytics & Metrics - Performance tracking and insights
- Audit Trails - Complete change history and compliance
- Error Handling - Robust failure recovery and retry logic
- Frontend Integration - JavaScript event handling patterns
- Order Workflow Example - E-commerce order processing
- Article Publishing Example - Content approval processes
- User Subscription Example - Subscription lifecycle management
Perfect for: Event-driven architectures, real-time applications, audit systems, integration platforms, and any application requiring comprehensive state change tracking and notifications.
π― 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