tapp/filament-form-builder
Filament plugin for building forms in the admin panel and collecting user data on the front end. Supports Filament field components, Laravel validation rules, response rendering, CSV export, sorting, and multi-tenancy-aware migrations.
1. Install (VCS, not Packagist)
Since the package isn't on Packagist yet, add the repository first, then require it:
{
"repositories": [
{ "type": "vcs", "url": "https://github.com/TappNetwork/Filament-Form-Builder" }
]
}
composer require tapp/filament-form-builder:"^4.0"
Check the version matrix before you start: Filament 4.x/5.x → package 4.x, Filament 3.x → package 1.x. Requirements are PHP 8.2+, Laravel 11+, and it pulls in maatwebsite/excel and spatie/eloquent-sortable transitively.
2. Publish and migrate
php artisan vendor:publish --tag="filament-form-builder-migrations"
php artisan migrate
If you want to customize behavior, also publish config/views:
php artisan vendor:publish --tag="filament-form-builder-config"
php artisan vendor:publish --tag="filament-form-builder-views"
3. Register the right plugin per panel
The package splits admin and front-end concerns into three plugins, and you almost certainly need at least two:
// AdminPanelProvider — manage forms, fields, entries
->plugins([FilamentFormBuilderPlugin::make()])
// GuestPanelProvider — public form + entry pages
->plugins([FilamentFormBuilderGuestPlugin::make()])
// AppPanelProvider — authenticated form + entry pages
->plugins([FilamentFormBuilderFrontendPlugin::make()])
4. First use case
Log into the admin panel, create a form, add fields (each has a required type plus Laravel validation rules), then hit /forms/{form} to submit it. A record lands in the entries list, viewable at /entries/{entry} and exportable to CSV from the Entries relation manager. That round trip — build in admin, submit on front end, export — is the whole package in one pass.
Where to look first
config/filament-form-builder.php — panel IDs, route URIs, login route, options editor, tenancyTapp\FilamentFormBuilder\Models\FilamentForm, FilamentFormField, FilamentFormUser — the data modelShowForm / ShowEntry page classes — the override points for presentationForm authoring workflow
Treat the form resource as a schema editor with a lifecycle: create → add/sort fields → lock. spatie/eloquent-sortable handles field ordering, and the locked flag lets you freeze a schema once real entries exist, so old submissions stay comparable. Lock a form before going live in production; edit freely only while it's unlocked and empty.
Field types and validation
Fields are native Filament components, so anything you'd write by hand you can assemble here — including repeaters and heading/layout fields (added in v1.5.0). Validation rules are plain Laravel rule syntax stored per field, so required|email|max:255 behaves exactly as it would in a FormRequest. This is the main appeal: no parallel schema/validation layer to keep in sync.
Options editor for choice fields
For Select/Radio/CheckboxList, prefer key_value (value => label) once you care about stable stored values vs. human labels:
'options_editor' => [
'RADIO' => 'key_value',
'SELECT' => 'tags',
'default' => 'tags',
],
The default tags mode stores label == value, which means renaming an option later desynchronizes historical entries. Choose key_value up front for anything long-lived.
Embedding a form in your own Livewire component
The package exposes its show component, and you should pass blockRedirect when nesting it so submission doesn't navigate away from your page:
@livewire('tapp.filament-form-builder.livewire.filament-form.show', [
'form' => $test->form,
'blockRedirect' => true,
])
Reacting to submissions
Two hooks, pick per context:
// Livewire parent component
protected $listeners = ['entrySaved'];
public function entrySaved($entryId) { /* re-resolve the entry */ }
// Laravel event listener
protected $listen = [
\Tapp\FilamentFormBuilder\Events\EntrySaved::class => [
\App\Listeners\HandleFormSubmission::class,
],
];
The event is the reliable integration point for notifications, CRM pushes, or fan-out to other models. Since v1.26 the Livewire event sends the entry id rather than the model, so re-fetch it in your handler.
Private entries via policy
This is opt-in per form and deliberately not a gate. Put the logic in a policy for FilamentForm with the exact method name:
public function viewEntries(User $user, FilamentForm $form): bool
{
if (! (bool) $form->private_entries) {
return true;
}
return $user->hasRole('Admin');
}
Then delegate from your FilamentFormUser policy so individual entry view links are gated too:
if ($entry->filamentForm?->private_entries) {
return $user->can('viewEntries', $entry->filamentForm);
}
This controls the Entries relation manager tab and the Export Selected bulk action.
Multi-tenancy
Configure tenancy before publishing/running migrations, then enable ->tenant(Team::class) on the panel. With tenancy on, queries scope automatically, URLs become /admin/{tenant-slug}/filament-forms, and tenant deletion cascades to forms, fields, and entries. The relationship_name and column overrides matter if your tenant relationship isn't the snake_case default.
Front-end presentation and export
For custom response rendering, override the ShowEntry page class via config rather than forking views, and use the published Blade views as your starting point (the results container carries a fb-form-user-container class for styling hooks). CSV export is a Filament bulk action over selected entries — good for ad-hoc pulls, less good as a scheduled reporting mechanism.
composer.lock and commit it. ^4.0 can resolve surprisingly if tags are sparse — verify with composer show tapp/filament-form-builder.blockRedirect, repeater/heading fields) describes the Filament 3 line. Don't apply 1.x migration snippets blindly to a 4.x install.ALTER TABLE for filament_forms, fields, and entries — plus backfilling. Decide on day one.viewEntries doesn't exist, the package doesn't restrict anything: every user who can view the form sees entries and can export. There's no warning, so add the policy as part of rollout, not after.viewEntries(User $user, FilamentForm $form): bool, called via can() on the owner record — not a gate registration. A typo silently degrades to "no restriction.""./vendor/tapp/**/*.blade.php" to your content array, otherwise dynamically assembled Filament field classes get purged and forms render unstyled. Also make sure the guest panel exists and its ID matches guest-panel-id, or public routes will 500/redirect oddly.permit_guest_entries enabled and the guest plugin registered on a real panel; authenticated users get redirected to the app panel, so test both states. php artisan route:list | grep -E 'forms|entries' is the fastest sanity check.FilamentFormUser records tied to filament_form_id; inspect the row directly and dump the validated payload in an EntrySaved listener. If a rule isn't firing, confirm the rule string parses standalone before blaming the plugin — it's passed straight into Laravel's validator.SetFormPanel middleware) → policy → events. Avoid editing published views as your primary customization path; you'll conflict on the next vendor:publish.maatwebsite/excel loads in-process. For thousands of entries, prefer your own queued export or chunked query over the bulk action.How can I help you explore Laravel packages today?