pvtl/voyager-forms
Voyager Forms adds a drag-and-drop form builder and submission management to Laravel's Voyager admin. Create dynamic forms, define fields, publish them, and review entries from the dashboard without custom code.
The package is a Voyager module, so treat Voyager setup as a prerequisite. You need a working Voyager 1.x install before anything here makes sense.
Install and publish.
composer require pvtl/voyager-forms
php artisan vendor:publish --provider="Pvtl\VoyagerForms\VoyagerFormsServiceProvider"
php artisan migrate
Publish tags usually cover config, migrations, views, and Voyager admin assets — publish all of them, then run php artisan voyager:admin so the menu entry and assets register.
Where to look first. Open config/voyager-forms.php (or whatever the published config is named) and the package's routes/ and resources/views/ directories in vendor/pvtl/voyager-forms. The render path and the submission endpoint are the two things you must understand before building anything on top. Then log into /admin and find the new Forms entry in the Voyager sidebar.
First use case: a contact form without a migration. Create a form in the admin, add name/email/message fields with validation rules, save, then render it on a frontend page using the package's Blade include or component (check the published views for the exact partial name and the variables it expects — typically the form slug or model). Submissions land in the package's table and are browsable from the admin, which replaces the usual "write a controller, a request class, a model, and a BREAD" loop.
If you only need one static contact form, you probably don't need this package — a plain controller is fewer moving parts. The value shows up at the third or fourth form.
Admin-first form definition. Build the form structure in Voyager, not in code. Fields carry type, label, validation rules, and options in the database, so non-developers can edit labels and add fields without a deploy. Keep a slug convention (contact, quote-request) and treat the slug as the stable contract your frontend references.
Embedding in existing pages. Rather than routing users to the package's standalone form page, resolve the form model in your own controller and pass it to the package's partial:
$form = \Pvtl\VoyagerForms\Models\Form::where('slug', 'contact')->firstOrFail();
return view('pages.contact', compact('form'));
This is the pattern that keeps your site's layout, SEO meta, and surrounding copy intact while the package only owns the <form> block and the POST handler.
Hooking submissions. The package persists submissions but won't know about your CRM, Slack channel, or mailing list. Attach behaviour via an Eloquent model event on the submission model (published or extended) or a listener on whatever event the package dispatches — check src/ for Event::dispatch calls. Keep side effects in queued listeners so a slow webhook doesn't fail a user's submission.
Custom field types. Field rendering is view-driven. Publish the views, copy an existing field partial in resources/views/vendor/voyager-forms/fields/, and add your own (date pickers, address blocks, conditional groups). Register the type where the package enumerates field types — usually a config array or a hardcoded list in a service provider; if it's hardcoded, an artisan vendor:publish plus a small override is the practical route.
Environment promotion. Because form definitions live in the database, staging-built forms don't travel with your repo. Either seed them (FormSeeder creating forms + fields from an array) or export/import the relevant tables as part of your deploy. For anything client-facing, seeding is worth the effort — otherwise a fresh production deploy has an empty Forms menu and a broken contact page.
Pair with a submission resource. Treat the package's admin submission list as read-only triage. If anyone needs filtering, CSV export, or status transitions, add your own BREAD or controller over the submissions table rather than fighting the module's UI.
It is archived. Last release April 2020. Pin laravel/framework and tcg/voyager in composer.json before installing, and expect friction on Laravel 9+. On modern stacks, fork it into packages/ with a path repository — you will need to patch it, and you can't rely on upstream.
Publish assets or the admin breaks. The Forms UI in Voyager needs its JS/CSS compiled and published. If the admin page renders blank or the field builder doesn't respond, it's almost always missing assets or a stale public/vendor/ directory after a voyager:admin run.
Validation lives in the database. Rules are stored as strings and applied dynamically. A typo in a rule string produces confusing runtime behaviour rather than a clear error, and required_if/sometimes style rules behave differently when applied outside a FormRequest. Test every rule you type. Consider a hardcoded allowlist of rule names if untrusted users can edit forms.
Mass assignment and escaping. Submission data is user input rendered back in the admin. Confirm the package escapes field values in its submission view; if not, escape in your own published views. Labels and help text authored in the admin are often rendered unescaped for formatting convenience — treat form-editing access as a trusted role.
File uploads depend on your disk config. If the package supports upload fields, check which disk and visibility it uses; the default may be public with a path that doesn't match your CDN or S3 setup. Also check whether uploads are validated for mime type and size — old Voyager modules frequently aren't.
Spam and rate limiting. There's no honeypot or throttling out of the box. Wrap the submission route with throttle: middleware and add a hidden field check in a listener that aborts before persisting. This is the single most common reason people end up forking.
Submissions table growth. No built-in pruning. Add an index on created_at and schedule a cleanup command; these tables quietly reach millions of rows on public-facing forms.
Debugging path. When a submission silently fails: check the route is registered (php artisan route:list | grep form), check CSRF token presence in the rendered form, check middleware groups in the config (web vs api), then check the submission model's $fillable against the field names — a mismatch drops fields silently rather than throwing.
Extension points worth knowing. Published views (full control over markup), the field-type registry (custom inputs), model events on the submission model (notifications, sync), and the config file (middleware, routes, storage). Everything else is a fork.
How can I help you explore Laravel packages today?