-
-
Notifications
You must be signed in to change notification settings - Fork 0
Events System
Build event-driven workflows with Laravel Statecraft's comprehensive event system.
Laravel Statecraft dispatches Laravel events during state transitions, allowing you to hook into the workflow lifecycle and create event-driven architectures.
The event system provides:
- Pre-transition events - Execute logic before state changes
- Post-transition events - React to completed state changes
- Full Laravel integration - Use Laravel's built-in event system
- Context preservation - Access transition context and metadata
Dispatched before a state transition occurs, allowing you to:
- Perform additional validation
- Prepare data for the transition
- Log transition attempts
- Cancel transitions if needed
use Grazulex\LaravelStatecraft\Events\StateTransitioning;
Event::listen(StateTransitioning::class, function (StateTransitioning $event) {
// Access event properties
$model = $event->model; // The model being transitioned
$from = $event->from; // From state
$to = $event->to; // To state
$transition = $event->transition; // Transition name
$context = $event->context; // Transition context data
// Example: Log transition attempt
Log::info("Attempting transition: {$from} -> {$to}", [
'model' => get_class($model),
'id' => $model->id,
'transition' => $transition
]);
});Dispatched after a state transition completes successfully:
use Grazulex\LaravelStatecraft\Events\StateTransitioned;
Event::listen(StateTransitioned::class, function (StateTransitioned $event) {
// Transition completed successfully
$model = $event->model; // The model that was transitioned
$from = $event->from; // From state
$to = $event->to; // To state
$transition = $event->transition; // Transition name
$context = $event->context; // Transition context data
// Example: Send notification
if ($to === 'shipped') {
Mail::to($model->customer_email)
->send(new OrderShippedMail($model));
}
});Configure events in your config/statecraft.php:
return [
'events' => [
'enabled' => true, // Enable/disable all events
// Specific event configuration
'state_transitioning' => true,
'state_transitioned' => true,
// Event queue configuration
'should_queue' => false, // Queue events for better performance
'queue_connection' => 'default',
'queue' => 'state-transitions',
],
];You can also configure events per state machine in YAML:
name: OrderStateMachine
model: App\Models\Order
initial_state: pending
config:
enable_events: true
queue_events: true
states:
# ... states definitionGenerate a listener:
php artisan make:listener OrderStateTransitionListenerRegister in EventServiceProvider:
<?php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Grazulex\LaravelStatecraft\Events\StateTransitioned;
use Grazulex\LaravelStatecraft\Events\StateTransitioning;
use App\Listeners\OrderStateTransitionListener;
use App\Listeners\OrderPreTransitionListener;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
StateTransitioned::class => [
OrderStateTransitionListener::class,
],
StateTransitioning::class => [
OrderPreTransitionListener::class,
],
];
}<?php
namespace App\Listeners;
use Grazulex\LaravelStatecraft\Events\StateTransitioned;
use App\Models\Order;
use App\Services\NotificationService;
use App\Services\InventoryService;
use App\Services\AuditService;
use Illuminate\Support\Facades\Log;
class OrderStateTransitionListener
{
public function __construct(
private NotificationService $notificationService,
private InventoryService $inventoryService,
private AuditService $auditService
) {}
public function handle(StateTransitioned $event): void
{
// Only handle Order model transitions
if (!$event->model instanceof Order) {
return;
}
// Log the transition
$this->auditService->logTransition($event);
// Handle specific transitions
match ($event->to) {
'paid' => $this->handlePaymentCompleted($event),
'processing' => $this->handleProcessingStarted($event),
'shipped' => $this->handleOrderShipped($event),
'delivered' => $this->handleOrderDelivered($event),
'cancelled' => $this->handleOrderCancelled($event),
default => null,
};
}
private function handlePaymentCompleted(StateTransitioned $event): void
{
$order = $event->model;
// Send payment confirmation
$this->notificationService->sendPaymentConfirmation($order);
// Reserve inventory
$this->inventoryService->reserveItems($order->items);
// Update order metadata
$order->update([
'payment_confirmed_at' => now(),
'inventory_reserved' => true
]);
}
private function handleProcessingStarted(StateTransitioned $event): void
{
$order = $event->model;
// Notify warehouse
$this->notificationService->notifyWarehouse($order);
// Create internal work order
$this->createWorkOrder($order);
}
private function handleOrderShipped(StateTransitioned $event): void
{
$order = $event->model;
$context = $event->context;
// Send shipping notification with tracking
$this->notificationService->sendShippingNotification($order, [
'tracking_number' => $context['tracking_number'] ?? null,
'carrier' => $context['carrier'] ?? null,
'estimated_delivery' => $context['estimated_delivery'] ?? null
]);
// Update inventory
$this->inventoryService->markAsShipped($order->items);
}
private function handleOrderDelivered(StateTransitioned $event): void
{
$order = $event->model;
// Send delivery confirmation
$this->notificationService->sendDeliveryConfirmation($order);
// Request review
$this->notificationService->requestProductReview($order);
// Complete inventory transaction
$this->inventoryService->completeTransaction($order->items);
}
private function handleOrderCancelled(StateTransitioned $event): void
{
$order = $event->model;
$context = $event->context;
// Send cancellation notification
$this->notificationService->sendCancellationNotification($order, [
'reason' => $context['reason'] ?? 'No reason provided'
]);
// Release inventory
$this->inventoryService->releaseReservedItems($order->items);
// Process refund if needed
if ($order->currentState() === 'paid') {
$this->processRefund($order);
}
}
}<?php
namespace App\Listeners;
use Grazulex\LaravelStatecraft\Events\StateTransitioning;
use App\Models\Order;
use Illuminate\Support\Facades\Log;
class OrderPreTransitionListener
{
public function handle(StateTransitioning $event): void
{
if (!$event->model instanceof Order) {
return;
}
// Log transition attempt
Log::info('Order state transition starting', [
'order_id' => $event->model->id,
'from' => $event->from,
'to' => $event->to,
'transition' => $event->transition,
'user_id' => auth()->id(),
'timestamp' => now()
]);
// Perform pre-transition validation
$this->validateTransition($event);
// Prepare data for transition
$this->prepareTransitionData($event);
}
private function validateTransition(StateTransitioning $event): void
{
$order = $event->model;
// Example: Additional business validation
if ($event->to === 'shipped' && !$order->shipping_address) {
throw new \Exception('Cannot ship order without shipping address');
}
if ($event->to === 'processing' && $order->items->isEmpty()) {
throw new \Exception('Cannot process order with no items');
}
}
private function prepareTransitionData(StateTransitioning $event): void
{
$order = $event->model;
// Example: Set transition metadata
if ($event->to === 'processing') {
$order->processing_started_at = now();
$order->assigned_warehouse = $this->determineWarehouse($order);
}
}
}Create specific event classes for different models:
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use App\Models\Order;
class OrderPaid
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public Order $order,
public array $paymentData = []
) {}
}<?php
namespace App\Actions;
use Grazulex\LaravelStatecraft\Contracts\Action;
use Illuminate\Database\Eloquent\Model;
use App\Events\OrderPaid;
class ProcessPaymentAction implements Action
{
public function execute(Model $model, array $context = []): void
{
// Process payment logic
$paymentResult = $this->processPayment($model, $context);
// Update model
$model->update([
'payment_processed_at' => now(),
'payment_id' => $paymentResult['payment_id']
]);
// Dispatch custom event
event(new OrderPaid($model, $paymentResult));
}
}Broadcast state transitions for real-time UI updates:
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use App\Models\Order;
class OrderStateChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public Order $order,
public string $previousState,
public string $newState
) {}
public function broadcastOn(): array
{
return [
new Channel('orders'),
new PrivateChannel('user.' . $this->order->customer_id),
];
}
public function broadcastWith(): array
{
return [
'order_id' => $this->order->id,
'previous_state' => $this->previousState,
'new_state' => $this->newState,
'timestamp' => now()->toISOString(),
];
}
}<?php
namespace App\Listeners;
use Grazulex\LaravelStatecraft\Events\StateTransitioned;
use App\Events\OrderStateChanged;
use App\Models\Order;
class BroadcastOrderStateChangeListener
{
public function handle(StateTransitioned $event): void
{
if ($event->model instanceof Order) {
broadcast(new OrderStateChanged(
$event->model,
$event->from,
$event->to
));
}
}
}<?php
namespace App\Listeners;
use Grazulex\LaravelStatecraft\Events\StateTransitioned;
use App\Models\Order;
use App\Services\CrmService;
use App\Services\AnalyticsService;
class ThirdPartyIntegrationListener
{
public function __construct(
private CrmService $crmService,
private AnalyticsService $analyticsService
) {}
public function handle(StateTransitioned $event): void
{
if (!$event->model instanceof Order) {
return;
}
// Update CRM
$this->crmService->updateOrderStatus($event->model, $event->to);
// Track analytics
$this->analyticsService->trackEvent('order_state_change', [
'order_id' => $event->model->id,
'from_state' => $event->from,
'to_state' => $event->to,
'transition' => $event->transition,
'customer_id' => $event->model->customer_id
]);
// Webhook notifications
if (in_array($event->to, ['paid', 'shipped', 'delivered'])) {
$this->sendWebhookNotification($event);
}
}
private function sendWebhookNotification(StateTransitioned $event): void
{
Http::post(config('webhooks.order_updates_url'), [
'event' => 'order.state_changed',
'order_id' => $event->model->id,
'state' => $event->to,
'timestamp' => now()->toISOString()
]);
}
}<?php
namespace Tests\Feature;
use App\Models\Order;
use Grazulex\LaravelStatecraft\Events\StateTransitioned;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
class OrderEventsTest extends TestCase
{
public function test_state_transitioned_event_is_dispatched()
{
Event::fake();
$order = Order::factory()->create();
$order->transition('pay');
Event::assertDispatched(StateTransitioned::class, function ($event) use ($order) {
return $event->model->id === $order->id &&
$event->from === 'pending' &&
$event->to === 'paid' &&
$event->transition === 'pay';
});
}
public function test_event_listener_handles_order_correctly()
{
Event::fake();
$order = Order::factory()->create();
$order->transition('pay');
Event::assertDispatched(StateTransitioned::class);
// Test that listener logic works
$this->assertNotNull($order->fresh()->payment_confirmed_at);
}
}<?php
namespace Tests\Unit\Listeners;
use App\Listeners\OrderStateTransitionListener;
use App\Models\Order;
use Grazulex\LaravelStatecraft\Events\StateTransitioned;
use Tests\TestCase;
class OrderStateTransitionListenerTest extends TestCase
{
public function test_handles_payment_completed()
{
$order = Order::factory()->create();
$event = new StateTransitioned($order, 'pending', 'paid', 'pay');
$listener = new OrderStateTransitionListener();
$listener->handle($event);
$this->assertNotNull($order->fresh()->payment_confirmed_at);
}
}- Separate concerns - Use different listeners for different responsibilities
- Queue heavy operations - Queue listeners that perform heavy operations
- Handle failures gracefully - Implement proper error handling
- Use specific events - Create custom events for complex scenarios
// Queue event listeners for better performance
class OrderStateTransitionListener implements ShouldQueue
{
use Queueable;
public function handle(StateTransitioned $event): void
{
// Heavy operations queued automatically
}
}class OrderStateTransitionListener
{
public function handle(StateTransitioned $event): void
{
try {
$this->processTransition($event);
} catch (\Exception $e) {
Log::error('Event handling failed', [
'event' => get_class($event),
'model_id' => $event->model->id,
'error' => $e->getMessage()
]);
// Optionally re-throw for critical operations
// throw $e;
}
}
}Now that you understand the event system, explore:
- π State History - Track and audit state changes
- π§ͺ Testing Guide - Test your event-driven workflows
- βοΈ Configuration Guide - Advanced configuration
- π¦ Order Workflow - Event-driven order processing
- π Article Publishing - Content workflow events
- π€ User Subscription - Subscription events
- π οΈ Console Commands - CLI tools for events
- π‘ Examples Collection - Complete examples
Ready to track state changes? Check out π State History!
π― 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