tomatophp/tomato-cms
Tomato CMS is a Laravel package from the TomatoPHP ecosystem that adds content management to your app: pages, posts, menus, and media, with a Filament admin panel. It plugs into TomatoPHP's module system for quick setup. Early-stage and lightly adopted, so review before production use.
Day-to-day with tomatophp/tomato-cms starts with the admin panel, not the codebase. The package is a Filament content module — pages, posts, categories, SEO metadata, multilingual fields — dropped into a Laravel app that already runs Filament (and ideally the wider Tomato PHP stack: admin, settings, roles, translations, media).
Where to look first, in order:
composer.json and the resolved tree. Filament major alignment is the gate. composer show --tree tomatophp/tomato-cms | tee deps.txt tells you what you actually signed up for.php artisan vendor:publish --provider="TomatoPHP\TomatoCms\TomatoCmsServiceProvider" — inspect config/tomato-cms.php and the migrations before php artisan migrate. Diff against any existing pages/posts/categories tables.TomatoPHP\TomatoCms\Models\* is the real contract — translated columns, slug source, publish flag, SEO fields. Read these before the Filament resources; the resources are generated ergonomics.First use case that pays off immediately: an internal back office or MVP where editors need page/post CRUD with slugs, publish toggles, categories, and SEO metadata inside a panel you'd otherwise hand-build. Prove one loop end-to-end: admin creates a page → publishes → front end renders it by slug. That loop is your whole value proposition; everything else is optimization.
# Throwaway branch, not main
composer require tomatophp/tomato-cms
php artisan vendor:publish --provider="TomatoPHP\TomatoCms\TomatoCmsServiceProvider"
# read config + migrations, then:
php artisan migrate
Wrap it behind your own seam from day one. With a 1-star, single-maintainer package, this is the highest-leverage pattern you'll write:
interface ContentRepository
{
public function pageBySlug(string $slug, string $locale): ?ContentPage;
public function publishedPosts(int $limit = 10): Collection;
public function menu(string $key): Collection;
}
final class TomatoContentRepository implements ContentRepository
{
public function pageBySlug(string $slug, string $locale): ?ContentPage
{
return ContentPage::query()
->where('slug', $slug)
->where('is_published', true)
->first();
}
}
Bind it in a service provider. Never let TomatoPHP\TomatoCms\Models\* leak into controllers or Blade beyond that boundary. It converts an existential dependency into an ordinary component swap.
Treat Filament resources as the editing surface, your Blade/Inertia as the reading surface. Let editors create and publish in the panel; render your own thin controllers calling ContentRepository on the front end. The package's shipped SEO routes are fine for a spike — but for production, owning the route contract keeps a package upgrade from silently changing your URLs.
Multilingual — verify the trait before writing locale-aware queries. Confirm whether translation is spatie/laravel-translatable under the hood (you get getTranslation()/setTranslation() and predictable fallback) or proprietary. If proprietary, read the trait end-to-end; fallback resolution is the thing that will bite you in a customer-visible way.
// Pattern: resolve a page in a locale with explicit fallback
$page = $repo->pageBySlug($slug, app()->getLocale())
?? $repo->pageBySlug($slug, config('app.fallback_locale'));
abort_unless($page, 404);
Cache the read side — it's the cheap win. pageBySlug(), menu trees, and settings lookups should be cached and invalidated on save. CMS read paths are trivially cache-friendly; the expensive mistakes are uncached menu trees firing on every request.
Cache::remember("page:{$slug}:{$locale}", 3600, fn () => $repo->pageBySlug($slug, $locale));
// Observer on save() → forget the tagged cache
Extend in your own namespace, never in vendor. Custom fields, workflow states, or extra Filament resources go in your own provider and your own namespace. Vendor edits are what make the eventual fork expensive.
Custom content types — lean on field definitions, don't invent a schema language. The package advertises flexible field definitions for custom content types. Use it for what it's designed for (blocks, structured metadata, repeatable fields). If you find yourself fighting the field system to model a real domain object, that's your signal to build a dedicated Eloquent model alongside the CMS rather than bending it.
Pin versions the moment the spike succeeds. composer require tomatophp/tomato-cms:1.x.x-style pinning (or a committed fork into your own namespace) keeps a stray composer update from pulling a breaking Filament major into prod.
composer why-not tomatophp/tomato-cms and read the lock diff. On a current stack, assume patching is required and budget the time.composer require is a proxy decision. Adopting this is adopting the Tomato PHP admin ecosystem (admin, settings, roles, translations, media). Make that call explicitly with your team rather than sliding into a multi-package program through one innocent line.pages/posts/categories are the loud failure mode. Diff first.slug, locale, and publish-status columns are indexed. Deep translation joins compound quietly over months.How can I help you explore Laravel packages today?