Install, register, and configure
Developer setup for a Laravel + Filament host. Admin form-building steps are in Workflows.
Requirements
- PHP 8.2+
- Laravel 11+
- Filament
^3.3|^4.0|^5.0(starter uses Filament v5 Schema: Section, Wizard, Tabs) - Livewire
^3.0|^4.0 - A
userstable (submissions and audit logs may FK to users) - Optional:
spiggle/dynamic-fieldsfor shared types and import - Optional: Filament Shield / Spatie Permission
- Optional:
spatie/laravel-activitylog(audit also writes to package tables without it)
Composer name: spiggle/form-builder. Namespace: Spiggle\FormBuilder. Plugin id: spiggle-form-builder.
Install (path repository)
{
"repositories": [
{
"type": "path",
"url": "packages/spiggle-form-builder",
"options": { "symlink": true }
}
],
"require": {
"spiggle/form-builder": "*"
}
}
composer require spiggle/form-builder:@dev
The service provider auto-registers. Livewire component name: form-builder.public-form.
Publish config, migrations, views
php artisan vendor:publish --tag=form-builder-config
php artisan vendor:publish --tag=form-builder-migrations
php artisan vendor:publish --tag=form-builder-views
php artisan migrate
Migrations also auto-load from the package. Tables: form_builder_forms, form_builder_submissions, form_builder_audit_logs (prefixed so they do not collide with a generic forms table). Publishing views is only needed if you want to restyle the public renderer.
For public file downloads on the public disk: php artisan storage:link.
Register the Filament plugin
Starter: app/Providers/Filament/AdminPanelProvider.php.
use Spiggle\FormBuilder\Filament\FormBuilderPlugin;
$panel
->plugin(\Spiggle\DynamicFields\Filament\DynamicFieldsPlugin::make()) // optional, recommended
->plugin(FormBuilderPlugin::make());
Registers two resources:
- Forms — navigation group Forms (sort 40), icon clipboard-document-list
- Submissions — same group (sort 41), icon inbox-stack
Public routing
Default: GET /forms/{path} (path can include slashes). Named route: form-builder.public. Only published and active forms resolve (scopePublished).
Environment:
FORM_BUILDER_ROUTE_PREFIX— defaultforms. Empty prefix serves at/{base_path}(easy to collide with/admin; keep a prefix unless you know the route table).FORM_BUILDER_ROOT_PATHS=true— registersResolveFormPathon thewebgroup so unmatched paths that match a published form’sbase_pathorslugstill render. Reserved first segments:admin,livewire,api,up,storage,vendor(configurable).
PathResolver rejects reserved segments, existing form slugs/paths, and existing Laravel routes, then appends -f{4-char-hash} (example style: contact-us-f8a1).
Permissions / Shield
AuthorizesFormBuilder::check($key) uses the same fallback as Dynamic Fields: permission or super_admin; if the permission row does not exist yet, authenticated panel users keep access.
| Config key | Default permission | Used for |
|---|---|---|
manage_forms | manage_forms | Forms resource |
view_submissions | view_form_submissions | Submissions resource (or manage_forms) |
export_submissions | export_form_submissions | Export actions |
manage_submissions | manage_form_submissions | Reserved for host policies |
php artisan shield:generate --all
Then assign those permissions to the roles that should manage forms vs only view/export submissions.
Artisan commands
| Command | What it does |
|---|---|
php artisan form-builder:seed |
Seeds Contact Us, Event Registration, Job Application, Customer Feedback (plus sample submissions). --fresh deletes those slugs first. |
php artisan form-builder:verify |
Checks tables, FieldCatalog, builder UX contracts, validation, sanitization, clone, routing, CSV/XLSX/PDF export. |
php artisan form-builder:export |
Portable JSON documents. Options: --slug=, --path= (default storage/app/form-builder-export.json). |
php artisan form-builder:import {path} |
Import documents. --publish marks them published. New unique slugs/paths are generated. |
Configuration
Published file: config/form-builder.php.
| Key | Default | Purpose |
|---|---|---|
schema_version | 1.0 | Portable document version |
route_prefix | forms | Public URL prefix |
root_paths | false | Vanity root interceptor |
reserved_paths | admin, livewire, api, up, storage, vendor | Never used as form paths |
submissions.statuses | new, reviewed, archived, spam | Inbox statuses |
submissions.store_ip | true | Store request IP |
submissions.hash_ip | false | SHA-256 the IP if stored |
submissions.store_user_agent | true | UA truncated to 512 chars |
submissions.sanitize_html | true | Strip tags; rich fields keep a small whitelist |
files.disk / directory / max_size_kb | public, form-submissions, 5120 | Public file uploads |
exports.disk / directory / queue / formats | local, form-builder-exports, default, csv/xlsx/pdf | Export files |
notify.enabled / mail_from | true, null | Reserved for host mailers — see Features |
container_types | single, wizard, tabs, pages | Layouts |
label_positions | above, inline, below, inside | Public labels |
field_types | same list as Dynamic Fields | Used when Dynamic Fields is absent |
drafts.enabled / session_key | true, form_builder_drafts | Wizard/pages session drafts |
validation_hooks | (unset) | Optional callable fn (Form, $rules): array |
Useful PHP API
use Spiggle\FormBuilder\Models\Form;
$form = Form::query()->published()->where('base_path', 'contact-us')->first();
$form->publicUrl();
$form->document(); // portable JSON object
$form->fields(); // flattened field arrays
$form->cloneForm(); // unpublished copy, new path
$form->importCustomFields([1, 2, 3]); // Dynamic Fields ids
app(\Spiggle\FormBuilder\Services\SubmissionManager::class)
->capture($form, $data, request(), ['source' => 'api']);