tomatophp/tomato-products
Tomato Products is a Laravel package that adds complete product management to your TomatoPHP admin panel. Create and organize product catalogs with categories, pricing, and media through a clean, ready-to-use interface.
Confirm the stack gate before typing anything. You need Laravel + a Filament v3 panel already running, plus the TomatoPHP core modules the package assumes (users/permissions, settings, translations). If your app is API-first, this is the wrong starting point — the value lives in the Filament resources, not a framework-agnostic domain model.
Minimal path:
composer require tomatophp/tomato-products:1.0.18 # pin, do not float ^1.0
php artisan vendor:publish --provider="TomatoPHP\TomatoProducts\TomatoProductsServiceProvider"
php artisan migrate
php artisan db:seed --class="TomatoPHP\TomatoProducts\Database\Seeders\TomatoProductsPermissionsSeeder"
php artisan filament:assets && php artisan optimize:clear
Then register the plugin in your PanelProvider (verify the exact class name in src/):
->plugin(\TomatoPHP\TomatoProducts\TomatoProductsPlugin::make())
Read these four things before writing a line of application code, in this order:
database/migrations/ — this is the real contract. Column names, nullability, decimal precision, unique constraints on SKU/slug, and whether SoftDeletes exists. Not the UI.src/Models/Product.php — relationships (categories, tags, media, variations/prices), casts, translatable traits.src/Filament/Resources/ProductResource.php — the form schema and rules(). Whatever validation lives here is the de facto schema contract for admin-created data.config/tomato-products.php — whether model classes, features, or route prefixes are configurable (this determines your extension strategy later).First use case to validate in a throwaway branch: seed a category, create a product with price/SKU/stock, attach media, publish, then read it back through php artisan tinker to confirm actual stored values (especially money and translatable fields). Do this with production-shaped data — a few thousand SKUs across nested categories — because Filament table performance issues only appear at volume.
Extend the model, never edit vendor code. Put App\Models\Product extends TomatoPHP\TomatoProducts\Models\Product in your app, add your extra fields via a companion one-to-one table (not by editing vendor migrations), and point the package at your subclass if the config exposes model keys. If it doesn't, extend and rebind in a service provider. This single habit keeps upgrades to a composer update instead of a fork.
Insert a repository/adapter layer immediately. One day of work now saves a data migration later:
interface CatalogRepository {
public function findPublishedBySku(string $sku): ?ProductData;
public function paginatePublished(int $perPage);
}
Storefront, ERP, reporting, and any future PIM should call this — never TomatoPHP\...\Models\Product::query() directly. Downstream systems binding to a third-party schema is the expensive mistake, not the install.
Mirror the Filament form in your API/form layer. Copy the rules() and field list from ProductResource into your own FormRequest so admin-created and API-created products can't drift. This is the cheapest consistency guarantee available.
Build your own Filament resource on the vendor model rather than forking the vendor resource. You'll want different columns, filters, and actions within weeks. Extend the vendor resource only if it's explicitly designed for it; otherwise a fresh resource on App\Models\Product is cleaner and won't break on patch bumps.
Route all writes through services. CatalogService::createProduct(), StockService::adjust(), PricingService::setPrice(). Seeding, imports, admin UI, and console commands should all call the same methods so observers, media conversions, and translatable handling fire identically. Product::create([...]) scattered across ten files is how stock totals diverge.
Handle translations explicitly. These packages lean on TomatoPHP localization / spatie/laravel-translatable. $product->name = 'X' typically writes only the current locale — use setTranslation() in imports and seeders, and always query with a locale fallback configured.
Permissions: map, don't duplicate. The permissions seeder creates package-specific permission names. Wire those strings into your existing roles model in a migration/seeder rather than building a parallel RBAC surface.
Testing: there are no vendor factories. Write your own ProductFactory on your App\Models\Product and put a single smoke test in CI — create a product, hit the Filament resource index as an admin, assert 200. That's your only early-warning system for opaque patch releases.
Pin exactly and audit every bump. 1.0.18, not ^1.0. There are no human-readable changelogs — v1.0.18 is just a compare link. Turn off Dependabot/Renovate for this package and instead run a scheduled review: git diff v1.0.17..v1.0.18 -- src database/migrations. Fifteen minutes per release, and it's the only way to catch a silent column type change.
Don't edit vendor migrations. If publishing makes them "yours," record that decision in the repo — because from then on, vendor migration changes will not apply and you'll have silent schema drift. The safer pattern is: leave vendor migrations untouched, add your columns/indexes in new migrations under database/migrations, and add indexes on sku, slug, and category_id yourself once the catalog grows.
Money arithmetic first. Check the migration: is price stored as decimal(10,2) or integer cents? Do not assume. Never sum prices with floats in PHP — cast to integer minor units at the boundary or use brick/money. Getting this wrong once is a very expensive bug.
Stock is a field, not a reservation system. The package gives you a stock column and variation rows. It does not do inventory orchestration, concurrency control, or holds. Any decrement that happens under concurrent load needs lockForUpdate() inside a transaction in your service — not a $product->decrement('stock') in a controller.
N+1 and table performance. Filament tables with media, categories, and variations eager-load poorly by default at scale. Override getEloquentQuery() in your resource with explicit with(['categories', 'media', 'variations']), and validate listing latency with realistic row counts before launch.
403s in the admin panel are almost always the permissions seeder. If the resource doesn't appear or throws 403 for a non-super-admin, re-run the seeder, php artisan permission:cache-reset, and check the policy. Also verify the plugin is registered after the auth/panel plugins in your PanelProvider.
Filament major upgrades are the real risk. A v3→v4 transition historically breaks plugin packages, and this one's upgrade timeline isn't yours. Before touching Filament's major version, confirm this package has a compatible release — otherwise you're choosing between a stale admin panel and a fork.
Handle import uniqueness deliberately. sku and slug usually carry unique constraints. Use updateOrCreate(['sku' => $sku], [...]) in importers and catch QueryException per row — a single duplicate SKU in a 5,000-row CSV will otherwise abort the whole import.
Debugging checklist: php artisan about for versions; php artisan route:list | grep products to confirm the panel routes exist; php artisan vendor:publish --provider=... with no args to list publishable tags before guessing them; config:cache cleared after any config publish in deploy.
Where the extension points most likely are: the published config file (model class mapping, feature toggles, route prefix), the plugin class (navigation group, resource registration), and Filament's resource lifecycle (custom actions, header widgets, table filters on your resource). Reach for those before reaching for a fork.
How can I help you explore Laravel packages today?