tomatophp/tomato-php
TomatoPHP core package gives Laravel developers a modular, open-source foundation for building admin panels and CMS applications, complete with ready-made CRUD generators, reusable plugins, and a growing ecosystem of tools for shipping dashboards faster. It is the heart of the TomatoPHP framework.
Install as a build-time tool, not a runtime dependency.
composer require --dev tomatophp/tomato-php:^1.1.8
Pin at 1.1.8 or later. Earlier tags emit a broken relation select in generated Splade forms (->remoteRoute('model.data'), an API that doesn't exist) and the generator still exits 0 — you only find out when an admin clicks the dropdown.
Prerequisites before running anything: a Laravel 10/11 app already running Splade or Breeze, Tailwind compiled, and at least one existing model + migration. This scaffolds models you own; it is not a schema designer.
Where to look first, in order:
php artisan list | grep -i tomato and php artisan vendor:publish --list | grep -i tomato to discover the real command and tag names. Public docs are thin — the command list is the ground truth.vendor/tomatophp/tomato-php/src — specifically the generator class, to answer two questions: does it overwrite or skip existing files, and how does it infer relations?First use case — the one-hour smoke test. Pick one model with a belongsTo and at least one relation select, generate it, then open every generated screen in a browser. Don't judge success by the exit code. If the relation dropdown renders and saves, the Splade path is healthy. If not, grep the emitted form for remoteRoute before debugging anything else.
Helpers: the package drops global helper functions into the autoload. Treat them as a bonus utility belt (formatting, config reads) — don't build domain logic on them, because they're the least portable thing if you ever remove the package.
Generate once, then own the output. The single most important habit. Generated files are a starting commit, not a managed artifact. Commit raw generator output as its own commit before hand-editing, so (a) your edits show as a readable diff and (b) re-running later is a reviewable comparison rather than an ambush. Never re-run against files you've since edited until you've verified the generator skips rather than clobbers.
Model-first workflow, repeated per entity:
make:model -m → write migration + relations + casts
→ run generator → commit raw output
→ review pass (checklist below) → hand-fix
→ wire routes/nav → add policy + FormRequest tests
→ render every page once
Fence the blast radius. Generate into a bounded namespace (app/Modules/Admin/… or a dedicated Admin directory) so generated-vs-handwritten is obvious in a diff and a bad run is one git revert plus a directory delete. Retrofitting into a mature domain layer is where this goes wrong.
The review checklist, encoded as a PR template. Every generated PR gets the same seven checks: mass-assignment guards ($fillable/$guarded), a policy or gate per action (index/create/update/delete), FormRequest validation actually emitted and used, eager loading on index queries, pagination present, middleware/CSRF wiring, and every page rendered once in a browser. Generated diffs get rubber-stamped precisely because they look plausible — the checklist is the counterweight.
Relation selects — the pattern to know. In Splade a relation select is a component contract, not a generic Laravel thing: the select points at a JSON endpoint via ->remoteRoot('data'), and that endpoint must return { data: [...] }. You own both halves — the form component and the JSON search endpoint/controller method. Audit both. A generated select with a working remoteRoot still fails if nothing returns the expected envelope.
Helpers for cross-cutting consistency, not logic. Use bundled helpers where they replace trivial repeated boilerplate so conventions stay uniform. Don't let helper calls leak into controllers near files the generator re-runs against — helpers are the part most likely to shift under you.
Version-pin alongside the framework. Pin tomatophp/tomato-php and Splade/Breeze versions together; don't float. The generator is coded against a snapshot of Splade's component API — the 1.1.8 fix is exactly what happens when that snapshot drifts. A Splade minor bump can silently invalidate emitted components the same way.
Cheap render smoke test. One authenticated test that GETs each generated route and asserts 200. It won't catch dropdown behavior, but it catches the class of failure this package has actually shipped: plausible, compile-passing output that breaks at render.
Sandbox spike recipe (1–2 days): three entities — one trivial, one belongsTo + hasMany, one with a pivot table and soft deletes — plus at least one relation select. Measure generated-vs-handwritten line ratio and log every defect. Start the defect log with "relation-select class" as row one.
The failure mode is the bad one. Generator exits 0, diff looks fine, page loads — then the relation select 500s at runtime when an admin opens the create form. As of 1.1.8 there is no integration test asserting generated code renders, so "it generated" and "it works" are separate claims you must verify separately, every time.
Remediation sweep for existing scaffolding. Before evaluating anything generated on an older tag: grep -rn "remoteRoute" app resources. Any hit is a broken relation select that passed review because it type-checks. Replace with ->remoteRoot('data') or regenerate that resource on 1.1.8.
Overwrite semantics are unknown until you check. Read the generator source for the file-existence branch. If it overwrites, never run it against a directory containing hand edits; if it skips, re-running to pick up new resources is safe. Don't guess — this is the difference between a convenience and a data-loss incident.
Breeze is a fork in the road, not one target. Confirm which variant you have (Blade, Livewire, Inertia). If your app has already diverged from the generator's assumed conventions, its value collapses to near zero. Splade is the better-trodden path — the one publicly visible bug was a Splade bug that got fixed.
Silent skips. Soft deletes, enum casts, translatable fields, media/file uploads, and non-trivial relations may be quietly ignored rather than erroring. Generate one entity per feature you care about during the spike and diff the output — absence is invisible unless you look for it.
Usual generated-code defects, untouched by any release: mass assignment left open, no authorization per action, N+1 queries in index tables, missing pagination and filtering. Assume all four are present until you've checked.
Debugging a broken relation select, in order: (1) grep the form for remoteRoute vs remoteRoot; (2) check the JSON endpoint's response envelope has a data key; (3) confirm the endpoint requires the right auth/middleware — a 302 to login looks like a "no results" empty dropdown in the UI.
Sibling-package drift is your likeliest composer update surprise. Single-maintainer namespace with sibling packages that have shipped the same relation-select bug (tomatophp/tomato-plugins#7). Keep the tomatophp/* constraint tight; don't let a transitive sibling float.
Verify the tags are real. Release metadata has included dates ahead of normal review windows. Confirm 1.1.8 is a published Packagist tag with a sane date before pinning — don't pin to a scheduled or mis-stamped entry.
Support model: GitHub issues, no SLA, one maintainer, ~33 stars. The escalation path is "read the stubs and fix it yourself," viable because the output is plain PHP you own. Publish your own stubs (or a small fork) if you standardize on this — that's the real extension point, far more durable than config flags.
Exit path stays cheap; protect it. require-dev only (exclude from production images), generated code in a fenced namespace, raw output committed separately from edits. Removal is then a git revert plus composer remove --dev. Write that sentence into the spike notes — otherwise the knowledge walks out with whoever ran it.
How can I help you explore Laravel packages today?