Persistence
EventMachine provides full event sourcing with automatic persistence to the database.
MachineEvent Model
All events are stored in the machine_events table:
use Tarfinlabs\EventMachine\Models\MachineEvent;
// Query events
$events = MachineEvent::where('machine_id', 'order')
->where('root_event_id', $rootId)
->orderBy('sequence_number')
->get();Event Properties
| Column | Type | Description |
|---|---|---|
id | ULID | Unique event identifier |
sequence_number | int | Order in event chain |
created_at | datetime | Event timestamp |
machine_id | string | Machine identifier |
machine_value | array | State after event |
root_event_id | ULID | Root of event chain |
source | enum | internal or external |
type | string | Event type name |
payload | array | Event data |
version | int | Event version |
context | array | Context changes (incremental) |
meta | array | State metadata |
Automatic Persistence
Events are automatically persisted when using Machine->send():
$machine = OrderMachine::create();
$machine->send(['type' => 'SUBMIT']);
// Event automatically saved to database
$machine->send(['type' => 'APPROVE']);
// Another event savedDisabling Persistence
For ephemeral machines or testing:
MachineDefinition::define(
config: [
'should_persist' => false,
'initial' => 'idle',
'states' => [...],
],
);Event History
Access history through the state:
$history = $machine->state->history;
// EventCollection methods
$history->count();
$history->first();
$history->last();
$history->pluck('type');
$history->where('source', 'external');Root Event ID
The root_event_id links all events in a machine instance:
$machine = OrderMachine::create();
// First event becomes the root
$machine->send(['type' => 'SUBMIT']);
$rootId = $machine->state->history->first()->root_event_id;
// All subsequent events share the same root_event_id
$machine->send(['type' => 'APPROVE']);
$machine->send(['type' => 'COMPLETE']);
// All events in this machine have the same root_event_id
$events = MachineEvent::where('root_event_id', $rootId)->get();
$events->count(); // 3 (plus internal events)State Restoration
Restore a machine from its root event ID:
// Save root ID somewhere (e.g., model attribute)
$rootId = $machine->state->history->first()->root_event_id;
// Later: Restore the machine
$restored = OrderMachine::create(state: $rootId);
// State is fully reconstructed
$restored->state->matches('approved'); // true
$restored->state->context->orderId; // Original valueIncremental Context Storage
Context is stored incrementally to minimize database size:
// First event: Full context
{
"context": {
"orderId": "order-123",
"items": [],
"total": 0,
"status": "pending"
}
}
// Second event: Only changes
{
"context": {
"items": [{"id": 1, "price": 100}],
"total": 100
}
}
// Third event: Only changes
{
"context": {
"status": "submitted"
}
}During restoration, context is reconstructed by merging all changes.
Transactional Events
Events can be wrapped in database transactions:
class CriticalEvent extends EventBehavior
{
public bool $isTransactional = true; // Default
public static function getType(): string
{
return 'CRITICAL_OPERATION';
}
}If any action fails, all database changes roll back:
try {
$machine->send(new CriticalEvent());
} catch (Exception $e) {
// All changes rolled back, including machine events
}Non-Transactional Events
For performance-critical operations:
class FastEvent extends EventBehavior
{
public bool $isTransactional = false;
}WARNING
Non-transactional events won't roll back on failure. Use with caution.
Distributed Locking
EventMachine uses distributed locking to prevent concurrent modifications:
// Machine A
$machineA = OrderMachine::create(state: $rootId);
// Machine B (same root_event_id)
$machineB = OrderMachine::create(state: $rootId);
// Concurrent access
$machineA->send(['type' => 'APPROVE']); // Acquires lock
try {
$machineB->send(['type' => 'REJECT']); // Waits for lock or throws
} catch (MachineAlreadyRunningException $e) {
// Handle concurrent access
}Lock Configuration
Lock timeout is 60 seconds with a 5-second wait:
// Behind the scenes
Cache::lock("machine:{$rootEventId}", 60)->block(5, function () {
// Process event
});Internal Events
EventMachine generates internal events for tracking:
$history = $machine->state->history;
// Filter internal events
$internal = $history->where('source', 'internal');
// Event types include:
// - 'order.machine.start'
// - 'order.state.pending.enter'
// - 'order.transition.start'
// - 'order.action.processOrder.finish'
// - 'order.guard.isValid.pass'Querying Events
By Machine Instance
$events = MachineEvent::where('root_event_id', $rootId)
->orderBy('sequence_number')
->get();By Machine Type
$events = MachineEvent::where('machine_id', 'order')
->latest()
->take(100)
->get();By Event Type
$submits = MachineEvent::where('type', 'SUBMIT')
->where('source', 'external')
->get();By Date Range
$events = MachineEvent::whereBetween('created_at', [$start, $end])
->get();External Events Only
$external = MachineEvent::where('root_event_id', $rootId)
->where('source', 'external')
->get();Manual Persistence
For MachineDefinition (without Machine class):
$definition = MachineDefinition::define([...]);
$state = $definition->getInitialState();
// Transition without persistence
$newState = $definition->transition(['type' => 'SUBMIT'], $state);
// Use Machine class for automatic persistenceDatabase Optimization
Indexing
The migration creates indexes on:
id(primary)root_event_idmachine_idcreated_at
Archival
For high-volume machines, enable archival:
// config/machine.php
'archival' => [
'enabled' => true,
'days_inactive' => 30,
'level' => 6,
],See Archival & Compression for details.
Best Practices
1. Store Root Event ID
class Order extends Model
{
protected function machines(): array
{
return [
'status' => OrderMachine::class . ':order',
];
}
}
// Column 'status' stores the root_event_id2. Use Transactions for Critical Operations
class PaymentEvent extends EventBehavior
{
public bool $isTransactional = true;
}3. Query Efficiently
// Good - indexed query
MachineEvent::where('root_event_id', $id)->get();
// Avoid - full table scan
MachineEvent::where('payload->orderId', 123)->get();4. Archive Old Events
php artisan machine:archive-events --days=30