laravel
How to Use the Verbs Package with FilamentPHP in Laravel
Integrating powerful event sourcing in Laravel has never been easier, thanks to the Verbs package. If you're building admin panels or internal tools with FilamentPHP, combining it with Verbs opens up an elegant way to manage application state using event-driven logic.
In this article, Iβll walk you through building a simple Todo application using FilamentPHP and the Verbs package, based on the open-source repository: github.com/tisuchi/filament-with-verbs.
#π Why Use Verbs with Filament?
- Event Sourcing: Every change to your model is recorded as an event.
- Auditability: You can replay or inspect changes to debug issues.
- Elegant state management: Your domain logic lives inside well-structured
Event
andState
classes.
#π§± Installation
Start by creating a fresh Laravel project:
laravel new filament-verbs-app
cd filament-verbs-app
Install required packages:
composer require filament/filament
composer require thunk/verbs
Publish Verbs migrations and config:
php artisan vendor:publish --tag=verbs-migrations
php artisan vendor:publish --tag=verbs-config
php artisan migrate
Install Filament:
php artisan make:filament-user
#π§ Creating the Todo Domain
We'll define a TodoState
that represents our aggregate's current status.
#1. Create the State Class
php artisan make:state Todo
In app/States/TodoState.php
:
namespace App\States;
use Thunk\Verbs\State;
class TodoState extends State
{
public int $todoId;
public string $title;
public ?string $description;
public bool $isCompleted;
public int $userId;
}
#2. Define Events
TodoCreated Event:
php artisan make:event TodoCreated
namespace App\Events;
use App\Models\Todo;
use Thunk\Verbs\Event;
use App\States\TodoState;
use Thunk\Verbs\Attributes\Autodiscovery\StateId;
class TodoCreated extends Event
{
#[StateId(TodoState::class)]
public int $todoId;
public string $title;
public ?string $description;
public bool $isCompleted;
public int $userId;
public function apply(TodoState $state)
{
$state->title = $this->title;
$state->description = $this->description;
$state->isCompleted = $this->isCompleted;
$state->userId = $this->userId;
}
public function handle(TodoState $state)
{
return Todo::create([
'id' => $this->todoId,
'title' => $state->title,
'description' => $state->description,
'is_completed' => $state->isCompleted,
'user_id' => $state->userId,
]);
}
}
TodoUpdated Event:
namespace App\Events;
use App\Models\Todo;
use Thunk\Verbs\Event;
use App\States\TodoState;
use Thunk\Verbs\Attributes\Autodiscovery\StateId;
class TodoUpdated extends Event
{
#[StateId(TodoState::class)]
public int $todoId;
public ?string $title;
public ?string $description;
public bool $isCompleted;
public function apply(TodoState $state)
{
$state->title = $this->title;
$state->description = $this->description;
$state->isCompleted = $this->isCompleted;
}
public function handle(TodoState $state)
{
return Todo::findOrFail($this->todoId)->update([
'title' => $state->title,
'description' => $state->description,
'is_completed' => $state->isCompleted,
]);
}
}
#π¨ Creating the Filament Resource
php artisan make:filament-resource Todo
In TodoResource.php
, override the create and update logic:
class TodoResource extends Resource
{
protected static ?string $model = Todo::class;
public static function form(Form $form): Form
{
return $form
->schema([
// Check details implementation there: https://github.com/tisuchi/filament-with-verbs/blob/main/app/Filament/Resources/TodoResource.php
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
// Check details implementation there: https://github.com/tisuchi/filament-with-verbs/blob/main/app/Filament/Resources/TodoResource.php
]);
}
}
#Update CreateTodo
page
namespace App\Filament\Resources\TodoResource\Pages;
+ use App\Events\TodoCreated;
class CreateTodo extends CreateRecord
{
+ // Override the create method to use events instead
+ protected function handleRecordCreation(array $data): Model
+ {
+ // Fire the TodoCreated event instead of creating the record through Filament
+ TodoCreated::commit(
+ todoId: snowflake_id(),
+ title: $data['title'],
+ description: $data['description'],
+ isCompleted: $data['is_completed'],
+ userId: Auth::id(),
+ );
+
+ // Find the most recently created Todo for the current user
+ // This is necessary because Filament expects a model to be returned
+ return \App\Models\Todo::where('user_id', Auth::id())
+ ->latest()
+ ->first();
+ }
}
#Update CreateTodo
page
namespace App\Filament\Resources\TodoResource\Pages;
+ use App\Events\TodoUpdated;
class EditTodo extends EditRecord
{
+ protected function mutateFormDataBeforeSave(array $data): array
+ {
+ TodoUpdated::commit(
+ todoId: $this->record->id,
+ title: $data['title'],
+ description: $data['description'] ?? null,
+ isCompleted: $data['is_completed'] ?? false,
+ );
+
+ return \App\Models\Todo::where('user_id', Auth::id())
+ ->latest()
+ ->first()
+ ->toArray();
+ }
}
Check details here how to use admin section.
#π Replaying Events
For some reason, you may need to replay and rebuild the data from your event and states. To do so, you need to truncate your todos
table.
β οΈβ οΈβ οΈ Please take a note that, you must not truncate 3 tables prefix with verb_
tables. If you mess with this tables, you will come out with some unusual results.
-
Before truncate the database, confirm that I have this record in the
todos
table. -
First, clear the current state by truncating the
todos
table (this simulates data loss in the domain table) -
Run the replay command to rebuild the state from events:
php artisan verbs:replay
-
Confirm that you want to run all events:
-
Successful replay notification:
-
Verify that the
todos
table has been fully reconstructed with exactly the same data that was there before:
This demonstrates the power of event sourcing - even if you lose your entire domain table, you can rebuild it completely from the stored events!
This will apply all events from the events
table and reconstruct your state objects.
#π§ͺ Try It Yourself
The full source code is available on GitHub: π github.com/tisuchi/filament-with-verbs
Run locally:
git clone https://github.com/tisuchi/filament-with-verbs.git
cd filament-with-verbs
cp .env.example .env
php artisan migrate --seed
php artisan serve
Login credentials:
- Email:
[email protected]
- Password:
password
#β Final Thoughts
Verbs and Filament make a perfect pair for building robust, maintainable admin panels powered by event-sourcing. You can easily extend this foundation into audit trails, workflow engines, and more.
Have any questions? Drop by Laravel School and letβs keep learning together!
Happy coding! π