-
-
Notifications
You must be signed in to change notification settings - Fork 0
Events System
Laravel Statecraft provides a comprehensive event system that allows you to listen for state changes and react accordingly. This enables you to build loosely coupled, event-driven applications.
Laravel Statecraft fires several events during the state machine lifecycle:
Fired when a state transition occurs successfully:
use Grazulex\LaravelStatecraft\Events\StateTransitioned;
// Event properties
$event->model; // The model instance
$event->fromState; // Previous state
$event->toState; // New state
$event->transition; // Transition name
$event->context; // Additional context data
Fired before a transition is attempted:
use Grazulex\LaravelStatecraft\Events\StateTransitionAttempted;
// Event properties
$event->model; // The model instance
$event->fromState; // Current state
$event->toState; // Target state
$event->transition; // Transition name
$event->context; // Additional context data
Fired when a transition fails (guard prevents it):
use Grazulex\LaravelStatecraft\Events\StateTransitionFailed;
// Event properties
$event->model; // The model instance
$event->fromState; // Current state
$event->toState; // Target state
$event->transition; // Transition name
$event->reason; // Failure reason
$event->context; // Additional context data
Add your listeners to EventServiceProvider
:
<?php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Grazulex\LaravelStatecraft\Events\StateTransitioned;
use App\Listeners\OrderStateChangedListener;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
StateTransitioned::class => [
OrderStateChangedListener::class,
],
];
}
<?php
namespace App\Listeners;
use Grazulex\LaravelStatecraft\Events\StateTransitioned;
use Illuminate\Support\Facades\Log;
class OrderStateChangedListener
{
public function handle(StateTransitioned $event): void
{
$model = $event->model;
$fromState = $event->fromState;
$toState = $event->toState;
Log::info("Order {$model->id} transitioned from {$fromState} to {$toState}");
}
}
<?php
namespace App\Listeners;
use Grazulex\LaravelStatecraft\Events\StateTransitioned;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Notification;
use App\Models\Order;
use App\Mail\OrderStatusChangedMail;
use App\Notifications\OrderUpdateNotification;
class OrderWorkflowListener
{
public function handle(StateTransitioned $event): void
{
// Only handle Order model events
if (!$event->model instanceof Order) {
return;
}
$order = $event->model;
$fromState = $event->fromState;
$toState = $event->toState;
// Handle specific state transitions
match ($toState) {
'paid' => $this->handlePaymentReceived($order),
'shipped' => $this->handleOrderShipped($order),
'delivered' => $this->handleOrderDelivered($order),
'cancelled' => $this->handleOrderCancelled($order, $fromState),
default => null
};
// Always log the state change
$this->logStateChange($order, $fromState, $toState);
}
private function handlePaymentReceived(Order $order): void
{
// Send payment confirmation
Mail::to($order->customer_email)->send(
new OrderStatusChangedMail($order, 'Payment Received')
);
// Notify warehouse for processing
Notification::route('slack', config('services.slack.warehouse_channel'))
->notify(new OrderUpdateNotification($order, 'Ready for processing'));
}
private function handleOrderShipped(Order $order): void
{
// Send tracking information
Mail::to($order->customer_email)->send(
new OrderStatusChangedMail($order, 'Order Shipped', [
'tracking_number' => $order->tracking_number
])
);
}
private function handleOrderDelivered(Order $order): void
{
// Request feedback
Mail::to($order->customer_email)->send(
new OrderStatusChangedMail($order, 'Order Delivered - Request Feedback')
);
// Update customer metrics
$order->customer->increment('completed_orders');
}
private function handleOrderCancelled(Order $order, string $fromState): void
{
// Send cancellation notice
Mail::to($order->customer_email)->send(
new OrderStatusChangedMail($order, 'Order Cancelled')
);
// If order was paid, initiate refund process
if (in_array($fromState, ['paid', 'processing'])) {
// Trigger refund workflow
dispatch(new ProcessRefundJob($order));
}
}
private function logStateChange(Order $order, string $from, string $to): void
{
$order->auditLogs()->create([
'event' => 'state_changed',
'from_state' => $from,
'to_state' => $to,
'user_id' => auth()->id(),
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
'timestamp' => now()
]);
}
}
You can also listen directly on your model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Grazulex\LaravelStatecraft\Traits\HasStateMachine;
use Grazulex\LaravelStatecraft\Events\StateTransitioned;
class Order extends Model
{
use HasStateMachine;
protected $stateMachine = 'OrderStateMachine';
protected static function booted()
{
// Listen for state transitions on this model only
static::observe(OrderObserver::class);
}
}
<?php
namespace App\Observers;
use App\Models\Order;
use Grazulex\LaravelStatecraft\Events\StateTransitioned;
class OrderObserver
{
public function stateTransitioned(Order $order, StateTransitioned $event): void
{
// Handle state transitions specific to Order model
if ($event->toState === 'paid') {
// Update inventory reservations
$this->reserveInventory($order);
}
if ($event->toState === 'cancelled') {
// Release inventory reservations
$this->releaseInventory($order);
}
}
private function reserveInventory(Order $order): void
{
foreach ($order->items as $item) {
$item->product->decrement('available_quantity', $item->quantity);
$item->product->increment('reserved_quantity', $item->quantity);
}
}
private function releaseInventory(Order $order): void
{
foreach ($order->items as $item) {
$item->product->increment('available_quantity', $item->quantity);
$item->product->decrement('reserved_quantity', $item->quantity);
}
}
}
Configure broadcasting in your config/statecraft.php
:
'events' => [
'enabled' => true,
'broadcast' => true,
],
<?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;
use App\Models\Order;
class OrderStatusUpdated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public Order $order,
public string $fromState,
public string $toState
) {}
public function broadcastOn(): array
{
return [
new PrivateChannel('orders.' . $this->order->customer_id),
new Channel('orders.public')
];
}
public function broadcastAs(): string
{
return 'order.status.updated';
}
public function broadcastWith(): array
{
return [
'order_id' => $this->order->id,
'from_state' => $this->fromState,
'to_state' => $this->toState,
'updated_at' => $this->order->updated_at->toISOString()
];
}
}
Broadcast from your listener:
public function handle(StateTransitioned $event): void
{
if ($event->model instanceof Order) {
broadcast(new OrderStatusUpdated(
$event->model,
$event->fromState,
$event->toState
));
}
}
<?php
namespace App\Listeners;
use Grazulex\LaravelStatecraft\Events\StateTransitioned;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class WebhookNotificationListener
{
public function handle(StateTransitioned $event): void
{
$model = $event->model;
// Only send webhooks for specific models
if (!$this->shouldSendWebhook($model)) {
return;
}
$webhookUrl = $this->getWebhookUrl($model);
if (!$webhookUrl) {
return;
}
$payload = [
'event' => 'state_changed',
'model' => get_class($model),
'model_id' => $model->getKey(),
'from_state' => $event->fromState,
'to_state' => $event->toState,
'transition' => $event->transition,
'timestamp' => now()->toISOString(),
'data' => $model->toArray()
];
try {
$response = Http::timeout(10)
->withHeaders([
'X-Webhook-Signature' => $this->generateSignature($payload),
'Content-Type' => 'application/json'
])
->post($webhookUrl, $payload);
if ($response->successful()) {
Log::info("Webhook sent successfully for {$model->getKey()}");
} else {
Log::warning("Webhook failed for {$model->getKey()}: " . $response->status());
}
} catch (\Exception $e) {
Log::error("Webhook error for {$model->getKey()}: " . $e->getMessage());
}
}
private function shouldSendWebhook($model): bool
{
// Configure which models should send webhooks
return in_array(get_class($model), [
\App\Models\Order::class,
\App\Models\Subscription::class
]);
}
private function getWebhookUrl($model): ?string
{
// Get webhook URL based on model or customer settings
return $model->customer->webhook_url ?? config('webhooks.default_url');
}
private function generateSignature(array $payload): string
{
return hash_hmac('sha256', json_encode($payload), config('webhooks.secret'));
}
}
<?php
namespace Tests\Feature;
use App\Models\Order;
use Grazulex\LaravelStatecraft\Events\StateTransitioned;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
class OrderStateEventsTest extends TestCase
{
public function test_state_transition_fires_event()
{
Event::fake();
$order = Order::factory()->create(['status' => 'pending']);
$order->transitionTo('pay');
Event::assertDispatched(StateTransitioned::class, function ($event) use ($order) {
return $event->model->is($order) &&
$event->fromState === 'pending' &&
$event->toState === 'paid';
});
}
public function test_failed_transition_fires_failed_event()
{
Event::fake();
$order = Order::factory()->create(['status' => 'pending']);
// Try invalid transition
try {
$order->transitionTo('delivered');
} catch (\Exception $e) {
// Expected to fail
}
Event::assertDispatched(StateTransitionFailed::class);
}
}
<?php
namespace Tests\Unit\Listeners;
use App\Listeners\OrderStateChangedListener;
use App\Models\Order;
use Grazulex\LaravelStatecraft\Events\StateTransitioned;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
class OrderStateChangedListenerTest extends TestCase
{
public function test_sends_email_when_order_is_shipped()
{
Mail::fake();
$order = Order::factory()->create([
'customer_email' => '[email protected]'
]);
$event = new StateTransitioned(
$order,
'processing',
'shipped',
'ship',
[]
);
$listener = new OrderStateChangedListener();
$listener->handle($event);
Mail::assertSent(OrderStatusChangedMail::class);
}
}
<?php
namespace App\Listeners;
use Grazulex\LaravelStatecraft\Events\StateTransitioned;
class ConditionalOrderListener
{
public function handle(StateTransitioned $event): void
{
$order = $event->model;
// Only handle events during business hours
if (!$this->isBusinessHours()) {
// Queue for later processing
dispatch(new ProcessOrderStateChangeJob($event))->delay(now()->addHours(1));
return;
}
// Handle immediately
$this->processStateChange($order, $event->fromState, $event->toState);
}
private function isBusinessHours(): bool
{
$now = now();
return $now->hour >= 9 && $now->hour < 17 && $now->isWeekday();
}
}
<?php
namespace App\Listeners;
use Grazulex\LaravelStatecraft\Events\StateTransitioned;
use Illuminate\Support\Facades\Cache;
class StateChangeAggregator
{
public function handle(StateTransitioned $event): void
{
$model = $event->model;
$cacheKey = "state_changes_{$model->getTable()}_{$event->toState}";
// Aggregate state changes for analytics
Cache::increment($cacheKey, 1);
Cache::expire($cacheKey, now()->addDay());
// Trigger alerts for unusual patterns
$count = Cache::get($cacheKey, 0);
if ($count > 100) { // Threshold for alerts
// Send alert for high volume of state changes
dispatch(new SendAlertJob("High volume of {$event->toState} transitions"));
}
}
}
<?php
namespace App\Listeners;
use Grazulex\LaravelStatecraft\Events\StateTransitioned;
use App\Models\StateTransitionMetric;
class StateTransitionAnalytics
{
public function handle(StateTransitioned $event): void
{
StateTransitionMetric::create([
'model_type' => get_class($event->model),
'model_id' => $event->model->getKey(),
'from_state' => $event->fromState,
'to_state' => $event->toState,
'transition_name' => $event->transition,
'user_id' => auth()->id(),
'occurred_at' => now(),
'context' => $event->context
]);
}
}
Now that you understand the Events System:
- Learn about State History - Track and audit all state changes
- Explore Console Commands - Use CLI tools for management
- See Real Examples - Complete workflow implementations
- Test Your Events - Ensure events work correctly
Ready to implement events? Check out our Event Usage Example for a complete implementation.
π― 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