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
php artisan verbs:install
This publishes the config and migrations. Run migrations afterward:
php artisan migrate
Define a State:
Create a state class (e.g., app/Domain/User/States/UserState.php):
namespace App\Domain\User\States;
use HireThunk\Verbs\State;
class UserState extends State
{
public string $name;
public int $age;
}
Define a Verb (Event):
Create a verb class (e.g., app/Domain/User/Verbs/UpdateUserName.php):
namespace App\Domain\User\Verbs;
use HireThunk\Verbs\Verb;
class UpdateUserName extends Verb
{
public function __construct(public string $newName) {}
}
Register the State and Verb:
In config/verbs.php, add your state and verb to the states and verbs arrays:
'states' => [
\App\Domain\User\States\UserState::class,
],
'verbs' => [
\App\Domain\User\Verbs\UpdateUserName::class,
],
First Use Case: Fire a verb in a controller or service:
use App\Domain\User\Verbs\UpdateUserName;
use HireThunk\Verbs\Facades\Verbs;
public function updateName(string $newName)
{
Verbs::fire(new UpdateUserName($newName));
}
app/Domain/ for organizing your states and verbs by domain.config/verbs.php for configuration options like event store, ID types, and metadata.database/migrations/ for customizing the event store schema.Define States and Verbs:
UserState).UpdateUserName, DeleteUser).Fire Verbs:
Use the Verbs facade to fire verbs:
Verbs::fire(new UpdateUserName('Alice'));
apply() methods (see below).Apply Verbs to States:
Override the apply() method in your state to handle verb logic:
class UserState extends State
{
public function apply(UpdateUserName $verb)
{
$this->name = $verb->newName;
}
}
Replay Events: Verbs handles replaying events to reconstruct state:
$userState = Verbs::replay(UserState::class, $userId);
Listen to Events:
Use Laravel's event listeners or Verbs' listen() method:
Verbs::listen(UpdateUserName::class, function (UpdateUserName $verb) {
// Side effects (e.g., notifications, analytics)
});
OrderState).class UserRepository
{
public function find(int $id): ?UserState
{
return Verbs::replay(UserState::class, $id);
}
}
Commit verbs just before rendering:
use HireThunk\Verbs\Facades\Verbs;
use HireThunk\Verbs\Livewire\CommitVerbs;
public function mount()
{
$this->commitVerbs = new CommitVerbs();
}
public function updatedName()
{
Verbs::fire(new UpdateUserName($this->name));
}
public function test_user_update()
{
Verbs::replay(UserState::class, $userId);
$this->assertEquals('Alice', $userState->name);
}
Verbs::fake() to mock verb firing:
Verbs::fake();
Verbs::shouldFire(UpdateUserName::class)->once();
Extend the default state factory for domain-specific logic:
namespace App\Domain\User\Factories;
use App\Domain\User\States\UserState;
use HireThunk\Verbs\StateFactory;
class UserStateFactory extends StateFactory
{
public function for(UserState $state, array $attributes = [])
{
return $state->replicate()->fill($attributes);
}
}
Register in config/verbs.php:
'factories' => [
UserState::class => \App\Domain\User\Factories\UserStateFactory::class,
],
Verbs::fire(new UpdateUserName('Alice'), [
'source' => 'admin_panel',
'user_id' => auth()->id(),
]);
config/verbs.php:
'snapshots' => true,
Snapshots store the current state after every event for faster reconstruction.Use optimistic_lock or pessimistic_lock in config/verbs.php:
'concurrency' => [
'guard' => 'optimistic_lock',
'column' => 'version',
],
Map HTTP requests to verbs in controllers:
public function update(Request $request, int $userId)
{
Verbs::fire(new UpdateUserName($request->name));
return response()->json(['status' => 'updated']);
}
Fire verbs in queue jobs:
class SendWelcomeEmail implements ShouldQueue
{
public function handle()
{
Verbs::fire(new UserRegistered($userId));
}
}
Use verbs to enforce API contracts:
public function createOrder(Request $request)
{
Verbs::fire(new CreateOrder(
$request->user_id,
$request->items,
$request->shipping_address
));
}
Listen to verbs to log changes:
Verbs::listen(function ($verb) {
\Log::info('Verb fired', ['verb' => $verb::class, 'data' => $verb]);
});
State Reconstruction Issues:
apply() methods or incorrect state property types.apply() methods in the state. Use php artisan verbs:generate:state to scaffold states.verbs_events table for missing events or malformed data.Circular Dependencies:
Concurrency Conflicts:
pessimistic_lock for critical sections or implement retry logic:
try {
Verbs::fire($verb);
} catch (\HireThunk\Verbs\Exceptions\ConcurrencyException $e) {
// Retry or handle conflict
}
Event Store Schema Mismatches:
Livewire State Persistence:
CommitVerbs middleware is registered and the component uses wire:model.live for reactive updates.Metadata Serialization:
How can I help you explore Laravel packages today?