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 Products Module Laravel Package

tomatophp/tomato-products-module

Products module for TomatoPHP adds complete product management to your Laravel admin panel—create, organize, and publish products with categories, pricing, inventory, images, and variants. Integrates smoothly with Filament and the TomatoPHP ecosystem.

View on GitHub
Deep Wiki
Context7

Getting Started

Assume the host, not the module, is the starting point. This package is a Filament-flavoured admin layer inside the TomatoPHP module ecosystem. Before you pull it in, confirm your app already has:

  • Laravel 10/11 and Filament 3.x installed and a panel provider registered.
  • The TomatoPHP module loader (tomatophp/* core) in place — this module assumes conventions from its siblings rather than bootstrapping a standalone admin.

Install and expose the moving parts:

composer require tomatophp/tomato-products-module

# the module ships config, migrations, lang and (usually) views
php artisan vendor:publish --tag=tomato-products-module-config
php artisan vendor:publish --tag=tomato-products-module-migrations
php artisan vendor:publish --tag=tomato-products-module-lang
php artisan migrate

List the available tags first (php artisan vendor:publish with no arguments) — tag names are the single most common source of "why is nothing published" confusion in this family of packages.

Where to look first, in order:

  1. composer.json → the require block. This tells you the exact Filament major, the sibling tomatophp/* packages it drags in (attributes, categories, media, translations), and therefore how much of your app this module is quietly coupled to.
  2. src/Models/ → the Product model and, critically, the variation/price/stock models. This is the real value of the package; read the relationships and column types before writing a single line of your own code. Confirm the namespace (Tomato\... vs TomatoPHP\...) here rather than guessing.
  3. database/migrations/ → the actual schema. Names, nullability, and which table owns price and stock decide everything downstream.
  4. src/Filament/Resources/ → how the admin is wired, what the form/table schemas look like, and where you'll hang your extension points.
  5. config/ and lang/.

First use case — get one product end-to-end before customising anything. Create a product in the admin panel with two variations, a price and a stock figure on each, then verify from the CLI what actually landed in the database:

php artisan tinker
>>> $p = \Tomato\Products\Models\Product::with('variations')->latest()->first();
>>> $p->toArray();
>>> $p->variations->map(fn ($v) => [$v->sku ?? $v->id, $v->price, $v->stock]);

If that round-trip works, you understand the data model well enough to build on it. If variations isn't the relationship name, you just learned the most important thing about this package for the price of one command.

Implementation Patterns

Treat the module as the write path, your app as the read path

The admin UI is the module's product. Your storefront, API, or downstream workflow should not reach into Filament resources — it should go through a thin read layer you own:

namespace App\Catalog;

use Tomato\Products\Models\Product;

class ProductRepository
{
    public function publishedWithPrices(int $perPage = 24)
    {
        return Product::query()
            ->where('is_active', true)          // confirm the actual flag name in migrations
            ->with(['variations', 'categories'])
            ->orderByDesc('created_at')
            ->paginate($perPage);
    }

    public function minPriceFor(Product $product): float
    {
        return (float) $product->variations->min('price');
    }
}

Wrapping the vendor model now costs an hour and buys you the ability to swap, fork, or replace the module later without touching controllers. Given a v1.0.x, single-maintainer package, this is not optional hygiene.

Variation is the SKU — put price and stock there

In every schema this package family ships, the product is the marketing record (name, description, media, category) and the variation is the thing you actually sell. Day-to-day consequences:

  • Never store the authoritative price on the product. If you need a "from £X" display price, derive it (min('price')) or maintain it as a denormalised cache you can rebuild.
  • Never decrement stock on the product. stock on a variation is the inventory unit.
  • If you need more than one or two variant axes (size × colour × material), stop and check the schema before committing. Generic modules often model variations as flat rows with a JSON or pivot of attributes; a matrix UI is your work, not theirs.

Own your inventory mutations

The module gives you a stock column. It almost certainly does not give you concurrency-safe reservation. Do not wire an order-placed hook straight to $variation->decrement('stock'). Put it in a service with a lock:

namespace App\Catalog;

use Illuminate\Support\Facades\DB;
use Tomato\Products\Models\ProductVariation;

class StockService
{
    public function reserve(int $variationId, int $qty): bool
    {
        return DB::transaction(function () use ($variationId, $qty) {
            $variation = ProductVariation::whereKey($variationId)
                ->lockForUpdate()
                ->firstOrFail();

            if ($variation->stock < $qty) {
                return false;
            }

            $variation->decrement('stock', $qty);

            return true;
        });
    }
}

This is the single highest-value piece of code you will write on top of this package. Budget for it in the spike.

Extend the Filament resources, don't fork them

Add columns and fields from your own panel provider or a dedicated CatalogServiceProvider, using Filament's global configuration hooks and subclasses:

use Filament\Tables\Table;
use Tomato\Products\Filament\Resources\ProductResource;

// global tweak, applies everywhere
Table::configureUsing(function (Table $table) {
    $table->defaultSort('created_at', 'desc');
});

// or a project-local resource that inherits the module's
class ProductResource extends \Tomato\Products\Filament\Resources\ProductResource
{
    public static function getNavigationGroup(): ?string
    {
        return 'Catalogue';
    }

    public static function table(\Filament\Tables\Table $table): \Filament\Tables\Table
    {
        return parent::table($table)->columns([
            ...parent::table($table)->getColumns(),
            // your extra columns
        ]);
    }
}

Then unregister the vendor resource from the panel and register yours. This survives composer update; editing vendor/ does not.

Schema changes go in your migrations

Need a barcode or a cost_price? Write database/migrations/xxxx_add_barcode_to_products.php against the module's table. Do not edit the published module migration — a fresh install of the module on another environment will reintroduce the pristine schema and your column will silently vanish from CI.

i18n

Publish lang/ and override strings in lang/vendor/tomato-products-module/{locale}/. The upstream release history here is translation-only commits, so package-level lang files are actively churning; treat anything you write inside the package directory as disposable.

Permissions and multi-tenancy

If you need team-scoped products, pair with tomato-roles / tomato-permissions and register your own policies in AuthServiceProvider rather than assuming defaults. For tenancy, add a team_id (or your tenant key) via your own migration plus a global scope on a subclass — the package won't ship awareness of your tenancy model.

API and seeding

If you need JSON endpoints, write your own resource controllers and JsonResource classes over the module's models. Likewise ship your own factories and seeders, resolving the model through the container (app(\Tomato\Products\Models\Product::class)) so a namespace change is a one-line fix rather than a grep across the codebase.

Testing

RefreshDatabase + the module's published migrations gives you a real schema in tests. Build two or three factories (product, variation, category) on day one — every subsequent test and seeder depends on them, and they double as executable documentation of what you believe the schema is.

Gotchas and Tips

Pin the exact tag. This is one module in a large tomatophp/* monorepo. A composer update on any sibling can drift the whole set — including Filament. Use "tomatophp/tomato-products-module": "1.0.2" (or a ~1.0.2 at most) and upgrade deliberately, reading composer.json diffs.

Filament major bumps break resource registration first. TomatoPHP modules track Filament 3.x. Before upgrading Filament in the host app, diff the module's require block and boot the panel on a branch. Expect the failure to surface as a blank or missing resource, not a clear exception.

"The resource doesn't appear in the panel" — checklist, in order:

  1. Is the module's service provider actually registered (bootstrap/providers.php, config/app.php, or the module's module.json)?
  2. Is the config enabled / navigation flag on after publishing?
  3. Does the resource's canViewAny() return true for the logged-in user? A missing policy returning false hides resources silently in Filament.
  4. Is your panel's resource discovery path including the vendor namespace, or have you registered resources explicitly?

Releases here are thin and often translation-only. v1.0.2 is literally "update translation". Before pulling, git diff v1.0.1..v1.0.2 — the changelog note is not a reliable scope signal, and translation-only patches are the safe ones. Conversely, never infer capability from a version bump; nothing in a lang commit implies stock, pricing rules, or checkout behaviour.

Migrations are additive and idempotent-by-fresh-install. Renaming a vendor column is a trap: the module reintroduces its own schema on fresh installs and in CI. Add columns, add indexes, add your own tables — don't mutate theirs.

Stock is a number, not an inventory system. There is no reservation, no backorder semantics, no multi-warehouse support in a "products module" of this size. If your roadmap needs any of that, this package is the wrong layer — write the inventory domain yourself alongside it.

Don't hard-code strings the package translates. If you find yourself writing labels inside a vendor resource, you are one composer update away from losing them. Override via the vendor lang path.

Debugging toolkit that actually pays off here: php artisan tinker against the models (fastest way to confirm relationship names and column reality), php artisan about for provider/panel state, Laravel Telescope for the query log while exercising the admin, and a throwaway test that hits the resource route to confirm policies.

Extension points, ranked by durability: your own migrations and subclasses (safe) → published config/lang/views (safe-ish, re-publish on upgrade) → Filament configureUsing hooks (safe) → model observers and repositories (safe) → editing vendor/ (never). The patch cadence on this package is fast enough that vendor edits are effectively temporary.

Escape hatch, defined before you adopt. Because the dependency is v1.0.x with one apparent maintainer, decide up front how you'd carry a fork: which of your files reference the vendor namespace, how many of them there are, and what the composer.json repositories VCS entry would look like. If that list is short — which it will be if you followed the repository/service pattern above — the risk is manageable. If it's long, stop and build the schema yourself; you'll spend less time than you'd spend adapting this one.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
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
cgv/localized-resource