hirethunk/verbs
Verbs is a PHP event sourcing package for Laravel artisans. It delivers the benefits of event sourcing while minimizing boilerplate and jargon, making it simpler to model behavior and build systems by thinking in actions (verbs) instead of nouns.
Installation:
composer require hirethunk/verbs
php artisan vendor:publish --tag=verbs-migrations
php artisan migrate
Generate Your First Event:
php artisan verbs:event UserRegistered
This creates a basic event class in app/Events/UserRegistered.php.
Fire an Event:
UserRegistered::fire(user_id: 1, email: 'user@example.com');
Define State (Optional):
php artisan verbs:state UserState
Customize app/States/UserState.php to track user-specific data.
Scenario: Track user sign-ups and validate no duplicate registrations.
// app/Events/UserRegistered.php
use HireThunk\Verbs\Attributes\StateId;
class UserRegistered extends Event
{
public function __construct(
#[StateId(UserState::class)]
public int $user_id,
public string $email,
) {}
public function validate(UserState $state): bool
{
return $state->email === null; // Prevent duplicate emails
}
public function handle()
{
// Create user in DB or trigger side effects
User::create(['email' => $this->email]);
}
}
Event-Driven Logic:
OrderPlaced, PaymentProcessed).handle() for side effects (e.g., sending emails, updating models).State Management:
#[StateId] or #[AppliesToState] to link events to states.last_login_at in UserState via UserLoggedIn event.#[AppliesToState(UserState::class)]
class UserLoggedIn extends Event {
public function apply(UserState $state) {
$state->last_login_at = now();
}
}
Validation:
validate() to enforce business rules (e.g., prevent over-subscription).public function validate(UserState $state): bool {
return $state->subscriptions_count < 3;
}
Nested States:
#[AppliesToChildState] for hierarchical data (e.g., Order → OrderItem).#[AppliesToChildState(
state_type: OrderItemState::class,
parent_type: OrderState::class,
id: 'item_id'
)]
class ItemAddedToOrder extends Event {
public function apply(OrderItemState $state) {
$state->quantity++;
}
}
Metadata:
team_id, ip_address) via Verbs::createMetadataUsing().Verbs::createMetadataUsing(fn (Metadata $metadata) => [
'team_id' => auth()->user()->team_id,
]);
Replay Safety:
#[Once] or use Verbs::unlessReplaying().#[Once(UserState::class)]
public function handle() {
// One-time logic (e.g., send welcome email)
}
handle() to sync state with Eloquent models.Verbs::fire() and assert state changes.
$this->expectsEvents(UserRegistered::class);
UserRegistered::fire(user_id: 1, email: 'test@example.com');
$this->assertDatabaseHas('users', ['email' => 'test@example.com']);
State Mismatches:
#[StateId] or #[AppliesToState] causes events to fire without state updates.php artisan verbs:event and php artisan verbs:state to scaffold correctly.Circular Dependencies:
null vs. exists).Metadata Overuse:
Replay Issues:
handle() (e.g., sending emails) may duplicate during replays.#[Once] or Verbs::unlessReplaying().Nested State Complexity:
AppliesToChildState chains are hard to debug.$history = Verbs::historyFor(UserState::class, $user_id);
// Inspect $history->events to see fired events.
dd(Verbs::stateFor(UserState::class, $user_id));
Verbs::lastError() for failed validations.Custom ID Generation:
Override Verbs::generateId() for non-snowflake IDs (e.g., UUIDs).
Verbs::generateIdUsing(fn () => Str::uuid());
Event Storage:
Extend Verbs\Storage\EventStorage to use custom databases (e.g., Redis).
Verbs::useStorage(new RedisEventStorage());
Metadata Providers: Register dynamic metadata via service providers.
Verbs::createMetadataUsing(fn () => ['locale' => app()->getLocale()]);
State Serialization:
Customize Verbs\State serialization for complex types (e.g., JSON fields).
class UserState extends State {
protected $casts = ['preferences' => 'json'];
}
OrderShipped) for clarity.*State suffixes (e.g., UserState) for auto-discovery.Verbs::fireBatch() for bulk operations.public function mount() {
$this->userState = Verbs::stateFor(UserState::class, auth()->id());
}
Verbs::rememberState().How can I help you explore Laravel packages today?