hirethunk/verbs
Verbs is a Laravel-friendly event sourcing package for PHP artisans that keeps the benefits of event sourcing while cutting boilerplate and jargon. Model behavior as verbs, record events, and build projections with a clean, approachable API.
Installation:
composer require hirethunk/verbs
Publish the config and migrations:
php artisan verbs:install
php artisan migrate
Define a State:
php artisan verbs:make State User
This generates a UserState class in app/States/UserState.php.
Define a Verb (Event):
php artisan verbs:make Verb User Registered
This creates app/Verbs/User/Registered.php with a handle() method.
First Use Case:
// In a controller or service
$user = UserState::create(); // Initializes state
$user->fire(new User\Registered($user, $email = 'user@example.com'));
config/verbs.php: Configuration for event stores, IDs, and serialization.app/States/: All state classes.app/Verbs/: All event classes.app/Listeners/: Optional event listeners.State Creation:
$user = UserState::create(['name' => 'John']);
Firing Events:
$user->fire(new User\Registered($user, $email));
Registered event, updating the state.Replaying Events:
$user = UserState::find($userId);
$user->replay(); // Rebuilds state from stored events
Service Layer:
class UserService {
public function registerUser(string $email) {
$user = UserState::create();
$user->fire(new User\Registered($user, $email));
return $user->id;
}
}
Livewire Integration:
public function mount() {
$this->user = UserState::find($this->userId);
$this->user->replay();
}
public function fireEvent() {
$this->user->fire(new User\Updated($this->user, $this->name));
}
Listeners:
class SendWelcomeEmail {
public function handle(User\Registered $event) {
Mail::to($event->email)->send(new WelcomeEmail());
}
}
Register in EventServiceProvider:
protected $listen = [
User\Registered::class => [SendWelcomeEmail::class],
];
State Factories:
UserState::factory()->for(User::class)->create();
Pending Events:
$pending = $user->pending(new User\Updated($user, 'New Name'));
if ($pending->isValid()) {
$pending->commit(); // Persists without immediate state update
}
Snapshots:
$user->snapshot(); // Stores current state to optimize replay
Metadata:
$user->fire(new User\Registered($user, $email), [
'ip_address' => request()->ip(),
]);
Event Store Configuration:
config/verbs.php has the correct event_store (e.g., database, doctrine, or custom).json type for the data column to avoid serialization issues:
$table->json('data')->nullable();
ID Generation:
uuid. Change in config/verbs.php:
'id_type' => 'snowflake', // or 'ulid'
voku/ulid-php or spatie/snowflake-id for non-UUID IDs.State Serialization:
$user->posts where Post has a $user back-reference).#[Verbs\Attributes\Ignore] to exclude properties:
#[Ignore] public $temporaryData;
Replay Side Effects:
fire() in handle()) are ignored by default. Use fireIfValid() or fireIfAllowed() for conditional firing:
$event->fireIfValid(); // Only fires if event is valid
Concurrency:
last_event_id. Ensure your event store supports this (e.g., last_event_id column in the event table).Event Lifecycle:
prepare, validate, apply, commit. Debug with:
$event->onPrepare(fn () => Log::debug('Preparing event'));
State Reconstruction:
Pending Events:
->isValid() to check if a pending event can be committed:
if (!$pending->isValid()) {
Log::error('Invalid event:', $pending->errors());
}
Custom Event Stores:
Hirethunk\Verbs\Contracts\EventStore for non-database stores (e.g., Redis, Kafka).Custom Serializers:
Hirethunk\Verbs\Serializers\Serializer for custom data formats (e.g., MessagePack).State Aliases:
#[Verbs\Attributes\Alias] to map state classes to simpler names:
#[Alias('user')] class UserState {}
Then access via UserState::for('user').Testing:
Verbs::fake() to mock events:
Verbs::fake();
$user->fire(new User\Registered($user, $email)); // Won't persist
Verbs::assertFired(User\Registered::class) or Verbs::assertNotFired().Livewire Hooks:
use Hirethunk\Verbs\Livewire\CommitPendingEvents;
public function mount() {
CommitPendingEvents::commit();
}
Snapshots:
$user->snapshot(); // Store current state every N events
config/verbs.php:
'snapshot_every' => 10, // Store snapshot every 10 events
Caching:
Verbs::stateManager()->clearCache();
Batch Processing:
Verbs::replay($stateId) for bulk state reconstruction (e.g., during deployments).How can I help you explore Laravel packages today?