rinvex/laravel-forms
Rinvex Laravel Forms is a database-driven form builder for Laravel, letting you define forms and fields dynamically, render them in views, and manage submissions without hardcoding HTML. It supports validation, customizable field types, and flexible storage.
Reality check first. This package is archived and marked abandoned. Before you invest a day in it, decide whether you are comfortable owning the code: fork it, pin the exact commit, and verify it installs against your Laravel/PHP version. If you are on Laravel 10+, expect dependency conflicts and plan to either patch composer.json constraints in a fork or use --ignore-platform-reqs only as a temporary diagnostic, never as a production strategy.
Minimal install:
composer require rinvex/laravel-forms
php artisan vendor:publish --provider="Rinvex\Forms\Providers\FormsServiceProvider"
php artisan migrate
Where to look first, in this order:
config/rinvex.forms.php — model bindings, table names, route prefix/middleware, field-type mappings. This is the main extension surface.database/migrations/* — the forms and form-field tables. Read the columns before writing any model code; the package is thin and the schema is the real API.src/Models/Form.php and src/Models/Field.php — relationships, accessors, and any render()/toArray() helpers.resources/views/vendor/rinvex/forms — this is where field markup lives and where you will almost certainly need to make changes.A realistic first use case is a database-driven contact form. Define it once in a seeder so it is version-controlled:
use Rinvex\Forms\Models\Form;
$form = Form::create([
'name' => 'Contact Us',
'slug' => 'contact-us',
'method' => 'POST',
'action' => route('contact.submit'),
]);
$form->fields()->createMany([
['name' => 'name', 'type' => 'text', 'label' => 'Name', 'validation' => 'required|string|max:255', 'sort' => 1],
['name' => 'email', 'type' => 'email', 'label' => 'Email', 'validation' => 'required|email', 'sort' => 2],
['name' => 'message', 'type' => 'textarea', 'label' => 'Message', 'validation' => 'required|string', 'sort' => 3],
]);
Then in a controller:
$form = Form::where('slug', 'contact-us')->firstOrFail();
return view('contact', compact('form'));
And in Blade, either use the package renderer if it suits your markup:
{!! $form->render() !!}
…or, more likely for day-to-day work, render the fields yourself so you control escaping and CSS:
<form method="{{ $form->method }}" action="{{ $form->action }}">
@csrf
@foreach ($form->fields->sortBy('sort') as $field)
<div class="form-group">
<label for="{{ $field->name }}">{{ $field->label }}</label>
@include("forms.fields.{$field->type}", ['field' => $field])
</div>
@endforeach
<button type="submit">Send</button>
</form>
Treat the package as a schema + renderer, not a full form framework. The durable pattern is: store form definitions in the database, render them through your own Blade partials, and own submission handling in application code. That keeps you insulated if you later replace the package.
Seeder-driven definitions for fixed forms. For forms that are part of the product (contact, application, survey), define them in seeders or migrations rather than through an admin UI. Use updateOrCreate keyed on slug so re-seeding is idempotent and field changes are reviewable in git.
Dynamic validation from field metadata. The most useful integration point is mapping stored validation strings onto Laravel’s validator:
$rules = $form->fields->mapWithKeys(function ($field) {
return [$field->name => $field->validation ?: 'nullable'];
})->all();
$data = $request->validate($rules);
If validation strings can be edited at runtime, parse them defensively (strip unknown rules, whitelist rule names) rather than passing arbitrary strings into the validator.
Field partials per type. Create resources/views/forms/fields/{text,email,textarea,select,checkbox,radio}.blade.php and keep each partial responsible for one input. This is the cleanest extension point: a new field type is a new partial plus a config entry, not a package fork.
Frontend/API integration. If you render with Vue, React, or Livewire, don’t use the package’s HTML renderer at all. Serialize definitions instead:
return response()->json([
'form' => $form->only(['name', 'slug', 'method', 'action']),
'fields' => $form->fields->map->only(['name', 'type', 'label', 'options', 'validation', 'sort']),
]);
The frontend renders controls; the backend still validates with the same rules. This is usually a better fit than server-rendered markup for anything interactive.
Submission persistence is yours. Unless you have verified otherwise in the source, assume the package does not provide a robust submissions table or spam handling. Create a form_submissions table with form_id, payload JSON, ip, and timestamps, and store $data there after validation. Add honeypot/rate-limiting at the controller level.
Admin CRUD. If you need non-developers to edit forms, build a small resource controller around Form and Field. Use nested resources (forms.fields) and a sortable list. Wrap edits in a transaction when reordering fields.
Testing. Write feature tests that post to your submission endpoint and assert both validation failures and successful persistence. Seed a form in the test and assert that every field name in the definition appears in the rendered HTML. This catches field/type mismatches early.
render() and relationship names against src/ before building on them.{!! $form->render() !!} is only safe if every label, placeholder, and option value is trusted. If form definitions are editable by users, escape labels in your own partials and never interpolate unescaped field attributes.(form_id, name) or validate in the admin layer. Duplicate names silently break validation because later rules overwrite earlier ones.sort/order column, and renumber on reorder rather than using sparse values.$fillable/$guarded on the package models before create() calls. If slug or action is guarded, your seeder will silently drop it.migrate in a shared database, and consider copying the schema into your own migrations if you need to modify it.is_array($field->options) ? ... : json_decode(...)) rather than assuming one format.php artisan config:clear after changes. Stale config is a common cause of “my custom model isn’t used” confusion.dd($form->toArray()) and dd($form->fields->toArray()) to confirm what the package actually loaded. If rendering fails, check whether views were published and whether the view namespace matches the config. Enable query logging to confirm the fields relationship is eager-loaded; N+1 queries on forms with many fields are easy to introduce.Form/Field), published views for markup, and your own field partials. Avoid editing vendor/ directly; every change there is lost on the next composer update.How can I help you explore Laravel packages today?