Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Tomato Forms Laravel Package

tomatophp/tomato-forms

Tomato Forms is a form builder for TomatoPHP and Filament that lets you create dynamic, database-backed forms and manage submissions from your admin panel, with reusable field components so you can add custom forms to any Laravel app without boilerplate.

View on GitHub
Deep Wiki
Context7

Getting Started

Install, publish, migrate — in that order, pinned.

composer require tomatophp/tomato-forms:1.1.19
php artisan vendor:publish --tag=tomato-forms-config
php artisan vendor:publish --tag=tomato-forms-migrations
php artisan migrate

Pin the exact version in composer.json and commit the lock file. Last release was 2024-05-09, zero dependents, single maintainer — a floating constraint is a future broken build with no upstream fix coming.

Register the plugin in your Filament panel provider (v3 shape; verify against your installed major):

use TomatoPHP\TomatoForms\TomatoFormsPlugin;

public function panel(Panel $panel): Panel
{
    return $panel->plugin(TomatoFormsPlugin::make());
}

Read these four things before writing integration code, in this order:

  1. database/migrations/* — this is the API. You'll see forms, fields, and whatever table backs user meta. Ten minutes here saves hours of guessing.
  2. src/Models/ — confirm the actual namespace and the relationship names (Form::fields(), how submissions attach to User). Guessed namespaces are the most common wasted hour with small packages.
  3. src/Filament/Resources/ — the builder UI enumerates every supported field type and the JSON keys written into the field's options/validation columns. Those keys are your rendering contract.
  4. Any route/controller file — answers the make-or-break question: is there a non-panel render and submit path, or is this admin-authoring only?

First use case: an internal intake form rendered in Blade. HR request, ops data capture, onboarding questionnaire — five fields, inside an authenticated area. Half a day, and it answers every downstream question at once:

php artisan tinker
>>> \TomatoPHP\TomatoForms\Models\Form::with('fields')->first();
>>> \App\Models\User::find(1)->meta; // confirm the relation name

If the form renders from a Blade loop and the meta rows come back namespaced, you're clear to build. If it 500s or the relations don't exist, stop and decide fork-vs-drop before writing integration code — don't discover it three features in.

Implementation Patterns

Put a repository between your app and the package tables. This is the single highest-leverage decision you'll make, more valuable than any config choice:

interface FormRepository
{
    public function findBySlug(string $slug): FormDefinition;
    public function submit(string $slug, array $payload, User $user): void;
}

interface MetaSubmissionRepository
{
    public function forUser(User $user, string $slug): array;
    public function all(string $slug): Collection;
}

The Filament authoring UI survives a storage swap; only the implementation moves. Given the maturity signal, assume you will eventually replace the meta writer with a typed submissions table — the repository is what makes that a one-class change instead of a grep across the codebase.

Namespace every meta key at submit time. Assume the default writes field keys straight into a shared user-meta store:

foreach ($form->fields as $field) {
    $key = sprintf('forms.%s.%s', $form->slug, $field->key);
    $user->meta()->updateOrCreate(
        ['key' => $key],
        ['value' => $payload[$field->key] ?? null],
    );
}

Never key on $field->id — IDs shift between environments on a fresh seed and you'll chase a phantom staging bug. Slug + field key is stable and greppable.

Re-validate server-side on submit; don't trust stored config. Build a validator from the field definitions yourself:

$rules = $form->fields->mapWithKeys(fn ($f) => [
    $f->key => $this->rulesFor($f), // json_decode($f->validation) + required/type defaults
])->toArray();

$validated = Validator::make($request->all(), $rules)->validate();

Then whitelist: iterate the form's field keys and pull only those out of $validated. Never pass $request->all() to the meta writer — mass assignment into a user-meta table is a privilege-escalation path if any key collides with a real attribute.

Render with an explicit type switch and an include fallback. Drive a Blade partial from match/@switch so adding a field type is one reviewable addition, not a package edit:

@foreach ($form->fields as $field)
  <div class="field">
    <label>{{ $field->label }}</label>
    @switch($field->type)
      @case('text')     <input name="{{ $field->key }}" value="{{ old($field->key, $values[$field->key] ?? '') }}"> @break
      @case('textarea') <textarea name="{{ $field->key }}">{{ old($field->key, $values[$field->key] ?? '') }}</textarea> @break
      @case('select')   <select name="{{ $field->key }}">…</select> @break
      @default @includeIf("forms.fields.{$field->type}")
    @endswitch
  </div>
@endforeach

The @includeIf fallback is your extension point for custom field types. No subclassing required.

Seed stable forms as code, not by hand in the panel. Anything that isn't genuinely admin-editable belongs in a seeder — it survives migrate:fresh, it's reviewable in a PR, and definition changes show up as diffs:

$form = Form::updateOrCreate(['slug' => 'onboarding'], ['name' => 'Onboarding']);
$form->fields()->updateOrCreate(
    ['key' => 'start_date'],
    ['type' => 'date', 'label' => 'Start Date', 'validation' => 'required|date'],
);

Read back through the repository, never through the meta relation directly. Every display path goes through forUser(). That's the seam that lets you segment historical responses when you later add schema versioning.

Test at the seam. Feature-test your FormRepository and your submit endpoint. Filament/Livewire resource tests against the builder are brittle across majors and test the code most likely to be forked away.

Gotchas and Tips

Filament major compatibility is the top risk. May 2024 predates v4. Check the package's own composer.json for the filament/filament constraint before you install — if it's ^3.0 and you're on v4, plan the fork up front rather than discovering it at panel boot with a 500.

Meta-key collisions are a silent data-loss bug. A field keyed phone or name landing in a shared meta namespace overwrites an unrelated attribute with no error and no log line. Audit the submit path for prefixes on day one; this is the highest-severity thing in the package.

Deleting a form or field strands meta rows. There is almost certainly no cascade and no cleanup path. Gate panel deletes behind soft-deletes or an explicit archive step, and write the orphan sweep:

// Run manually first, then schedule it.
UserMeta::where('key', 'like', 'forms.%')
    ->get()
    ->filter(fn ($m) => ! Field::where('form_slug', $m->form_slug)
        ->where('key', $m->field_key)->exists())
    ->each->delete();

Stored XSS via admin-authored labels and options. Labels, help text, and select option strings are written by admins and rendered to users. Use {{ }} everywhere in your own renderer, and audit the package's shipped views for {!! !!} — a careless or compromised admin account becomes a persistent XSS vector against your whole user base.

No schema versioning means reports lie after an edit. Rename a key or retype a field and historical submissions stop mapping to the current definition — silently. Two mitigations: never rename a key (add a new field, hide the old), and stamp each submission with the form definition's updated_at at write time so you can at least segment cohorts.

Validation is stored config, not a DB constraint. Everything lands in JSON, so a bad field definition becomes a data-integrity incident with zero schema-level defense. Add a ->rules() on the builder resource's form to validate the definition itself on save, and re-validate values at submit as shown above.

File uploads are the sharpest unknown. Don't assume disk, path, or retention. Verify which disk the package writes to, whether files are deleted when a submission is removed, and whether MIME/extension is validated. Budget for implementing this yourself.

Debugging moves, in order:

  • DB::listen() while submitting once shows exactly which tables and keys the package touches — do this before reading any source.
  • A "why won't it validate" bug is almost always the field's validation JSON; json_decode($field->validation) in tinker and read it.
  • A 500 on the panel resource right after install is nine times out of ten a Filament version mismatch or an unpublished migration, not your code.

Extension points worth knowing: custom field types via Blade partials and the repository seam. Avoid subclassing the Filament resources — that's the code most likely to break on a Filament bump, and subclassing welds you to it. Register your own resources over the same tables instead.

Keep it off the critical path. Ship internal-only first: nothing customer-facing, revenue-critical, or PII-bearing should sequence behind a package with zero dependents and one maintainer. The surface is small (two resources, a few migrations, likely one controller) and MIT-licensed — fork-and-own is the realistic endgame, so structure your code as if you already own it.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource