tomatophp/tomato-themes
Theme manager for TomatoPHP and Laravel applications: register, switch, and organize frontend themes with view and asset overrides, plus a simple API for building theme-aware admin panels and sites. Part of the TomatoPHP ecosystem.
Confirm the stack before you install. This package is a presentation-layer add-on for the TomatoPHP/Filament admin ecosystem. It assumes Laravel 10/11, PHP 8.1+, and a Filament 3 panel. If your app is bespoke Blade/Inertia/Livewire with no Filament panel, stop here — the abstraction buys you nothing.
Install and publish.
composer require tomatophp/tomato-themes
php artisan vendor:publish --tag=tomato-themes-config
php artisan vendor:publish --tag=tomato-themes-assets # tag name: verify in repo
Service provider registration is via Laravel auto-discovery; you should not need to touch config/app.php. Pin with a caret on the minor (^1.2) but commit composer.lock.
Where to look first, in this order:
config/tomato-themes.php or similarly named) — this is the single source of truth for the default/active theme and the directory conventions. Read it end to end before writing any theme code.php artisan list | grep -i theme. The description promises scaffolding and publishing commands; the exact signatures are not documented in the v1.2.2 notes, so treat the command help output as your API reference.First use case — prove the resolution path end to end. Scaffold a throwaway theme, activate it, and confirm a known Blade marker renders on a real route. Something like:
php artisan theme:make demo # confirm actual command name via `artisan list`
php artisan theme:publish demo
Put a unique string in the theme's layout (e.g. <!-- theme: demo -->), hit the page, and view-source. Do this before you build anything real. If the marker does not appear, the problem is activation, not your theme — and that distinction saves hours.
Ramp-up expectation: a Laravel developer is productive in under a day. The concepts (published views, asset registration, panel plugin) are idiomatic. The slow part is governance — deciding who adds themes and where they live — not the code.
Keep theme definitions in your repository, not the package's published directory. The published theme directory is effectively a vendor artifact. If you let it become the home of your themes, a patch release that renames an asset path or view namespace silently overwrites or orphans your work. Treat the published directory as a template; copy what you need into resources/themes/ (or your chosen path) under version control, and point the config at it.
Fallback chain, written down. The value of a theme manager is the fallback: theme view → app view → package view. Establish and document which layer wins for each resource type. In practice this is Laravel's view finder path ordering, so the resolution rule is "whatever the package prepends." Verify it once with:
dump(app('view')->getFinder()->getPaths());
dump(app('view')->getFinder()->getHints());
Then write that ordering into your team docs. Ambiguity here is the number-one source of "why is my override not showing up."
Activation — pick one primary mechanism and layer the rest on top.
Do not mix all three casually. Define the precedence in one place (ThemeResolver::resolve() wrapping the package) so you have a single seam to change later.
Per-tenant theming is not what this package does. It manages deployment-level themes. If per-tenant theming is a product feature, build a thin resolver layer on top of the runtime API and keep tenant→theme mapping in your own database. Do not expect the package to store that mapping.
Assets: publish, then cache-bust. Themes ship assets into public/. After every deploy that touches theme assets you need a publish step plus cache-busting. If the package's asset helper does not add a version query string, wrap it:
<link rel="stylesheet" href="{{ theme_asset('css/app.css') }}?v={{ config('app.asset_version') }}">
If your frontend is on Vite, assume the package's asset pipeline is not Vite-aware until you confirm otherwise. The safe pattern is to let Vite build into the theme's public directory and have the theme's layout reference the Vite manifest, rather than relying on the package's asset publishing for compiled CSS/JS.
Filament panel integration. Register the plugin on the panel in your PanelProvider. If the package offers theme presets for the panel itself, set them there, not in a service provider — panel configuration is evaluated at panel boot, and setting it elsewhere is a race you will lose intermittently.
Testing pattern — the cheapest insurance you can buy. One smoke test that asserts a known theme marker renders on a known route:
public function test_active_theme_renders(): void
{
config(['tomato-themes.active' => 'demo']);
$this->get('/')->assertSee('theme: demo', false);
}
A patch release that renames a view namespace or config key will make the theme fall back to defaults silently. Without this test, you find out during a demo.
Development loop. Symlink a working theme into the published directory while iterating, and keep the canonical copy in your repo. Delete the symlink before you cut a release so the deploy step is deterministic.
Stale view cache after switching is the most common failure. Laravel compiles Blade to disk; a theme switch does not necessarily invalidate it. In local development, run php artisan view:clear after any activation change. In production, make view:clear (and config:clear, before config:cache) an explicit deploy step.
config:cache in production interacts badly with runtime activation. If activation is read from config at boot, cached config freezes it. If activation is applied at runtime through the package's API, make sure that call happens after config is loaded and is not itself reading a runtime-mutated config value. Pin this down in a staging test; it is the difference between a working multi-theme deploy and a mysterious "always the default theme" bug.
View path precedence is decided at boot, not per request. Prepending a theme's view path is not free to redo mid-request. If you need per-route switching, do it in middleware that runs before the view is rendered, and do not expect to switch twice in one request.
Silent breakage on patch releases. The v1.2.2 release notes are a changelog link and nothing else — no summary, no breaking-change labels. A patch bump conventionally implies backward compatibility, but you cannot confirm that from the metadata. Before upgrading, read the raw compare diff and specifically check whether it touches: config keys, view namespaces, or asset manifest paths. Any of those three will break a live theme without erroring. Record the diff review in the PR so the next maintainer inherits the rationale.
Dependency drift with Filament. Theme packages accumulate implicit coupling to Filament majors. Run composer why-not tomatophp/tomato-themes <filament-version> before merging a Filament upgrade. If both an upgrade and a theme change are pending, land the Filament upgrade first and re-validate the theme package against it — coupling the two makes any failure unattributable.
Extension points: prefer wrapping to forking. Put your own ThemeResolver / ThemeManager service in front of the package. It gives you a place to add tenant scoping, logging, and fallback policy without touching vendor code. Only fork if you need to change how themes resolve, and decide to fork before you customize — vendoring under your own namespace after you have built on top is painful.
Debugging checklist when a theme "does not load":
dump(app('view')->getFinder()->getPaths()) — is the theme path actually registered?config:cache stale? (php artisan config:clear)php artisan view:clear)public/.Walk that list in order; it resolves the overwhelming majority of reports.
Watch container cold-start. If theme discovery happens at boot, measure it under PHP-FPM. It is unlikely to be a first-order bottleneck, but it is a boot-path cost, and boot-path costs compound with package count.
Support posture. Community-only, single maintainer, no SLA. Fine for internal admin tooling. If the panel is customer-facing, keep a fork contingency ready and do not let theme definitions live only inside the package's published directory — that is what makes abandonment recoverable rather than a migration project.
How can I help you explore Laravel packages today?